Merge pull request #5152 from ByteHamster/message-header-view
Show message headers in fragment
This commit is contained in:
commit
3cd28c5b2c
26 changed files with 447 additions and 322 deletions
|
@ -11,4 +11,5 @@ val mailStoreModule = module {
|
|||
single { K9BackendStorageFactory(get(), get(), get(), get()) }
|
||||
factory { SpecialLocalFoldersCreator(preferences = get(), localStoreProvider = get()) }
|
||||
single { MessageStoreProvider(messageStoreFactory = get()) }
|
||||
single { MessageRepository(preferences = get(), localStoreProvider = get()) }
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import com.fsck.k9.mail.internet.MimeMessage;
|
|||
import com.fsck.k9.mail.internet.MimeMultipart;
|
||||
import com.fsck.k9.mail.internet.MimeUtility;
|
||||
import com.fsck.k9.mail.internet.SizeAware;
|
||||
import com.fsck.k9.mail.message.MessageHeaderCollector;
|
||||
import com.fsck.k9.mail.message.MessageHeaderParser;
|
||||
import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
|
||||
import com.fsck.k9.mailstore.LockableDatabase.WrappedException;
|
||||
|
@ -660,7 +661,7 @@ public class LocalFolder {
|
|||
}
|
||||
|
||||
private void parseHeaderBytes(Part part, byte[] header) throws MessagingException {
|
||||
MessageHeaderParser.parse(part, new ByteArrayInputStream(header));
|
||||
MessageHeaderParser.parse(new ByteArrayInputStream(header), part::addRawHeader);
|
||||
}
|
||||
|
||||
public String getMessageUidById(final long id) throws MessagingException {
|
||||
|
|
|
@ -131,7 +131,7 @@ public class LocalMessage extends MimeMessage {
|
|||
|
||||
byte[] header = cursor.getBlob(LocalStore.MSG_INDEX_HEADER_DATA);
|
||||
if (header != null) {
|
||||
MessageHeaderParser.parse(this, new ByteArrayInputStream(header));
|
||||
MessageHeaderParser.parse(new ByteArrayInputStream(header), this::addRawHeader);
|
||||
} else {
|
||||
Timber.d("No headers available for this message!");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package com.fsck.k9.mailstore
|
||||
|
||||
import com.fsck.k9.controller.MessageReference
|
||||
import java.lang.RuntimeException
|
||||
|
||||
class MessageNotFoundException(val messageReference: MessageReference) :
|
||||
RuntimeException("Message not found: $messageReference")
|
|
@ -0,0 +1,40 @@
|
|||
package com.fsck.k9.mailstore
|
||||
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.Preferences
|
||||
import com.fsck.k9.controller.MessageReference
|
||||
import com.fsck.k9.mail.Header
|
||||
import com.fsck.k9.mail.internet.MimeHeader
|
||||
import com.fsck.k9.mail.message.MessageHeaderParser
|
||||
|
||||
class MessageRepository(
|
||||
private val preferences: Preferences,
|
||||
private val localStoreProvider: LocalStoreProvider
|
||||
) {
|
||||
fun getHeaders(messageReference: MessageReference): List<Header> {
|
||||
val accountUuid = messageReference.accountUuid
|
||||
val account = preferences.getAccount(accountUuid) ?: error("Account not found: $accountUuid")
|
||||
return account.database.execute(false) { db ->
|
||||
db.rawQuery(
|
||||
"SELECT message_parts.header FROM messages" +
|
||||
" LEFT JOIN message_parts ON (messages.message_part_id = message_parts.id)" +
|
||||
" WHERE messages.folder_id = ? AND messages.uid = ?",
|
||||
arrayOf(messageReference.folderId.toString(), messageReference.uid),
|
||||
).use { cursor ->
|
||||
if (!cursor.moveToFirst()) throw MessageNotFoundException(messageReference)
|
||||
|
||||
val headerBytes = cursor.getBlob(0)
|
||||
|
||||
val header = MimeHeader()
|
||||
MessageHeaderParser.parse(headerBytes.inputStream()) { name, value ->
|
||||
header.addRawHeader(name, value)
|
||||
}
|
||||
|
||||
header.headers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Account.database: LockableDatabase
|
||||
get() = localStoreProvider.getInstance(this).database
|
||||
}
|
|
@ -285,6 +285,10 @@
|
|||
android:name=".ui.settings.account.AccountSettingsActivity"
|
||||
android:label="@string/account_settings_title_fmt" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.messagesource.MessageSourceActivity"
|
||||
android:label="@string/show_headers_action" />
|
||||
|
||||
<receiver
|
||||
android:name=".service.StorageReceiver"
|
||||
android:enabled="true">
|
||||
|
|
|
@ -9,6 +9,7 @@ dependencies {
|
|||
api "androidx.appcompat:appcompat:${versions.androidxAppCompat}"
|
||||
api "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}"
|
||||
api "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}"
|
||||
api "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}"
|
||||
|
||||
implementation "androidx.core:core-ktx:${versions.androidxCore}"
|
||||
implementation "com.jakewharton.timber:timber:${versions.timber}"
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package com.fsck.k9.ui.base.loader
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
const val LOADING_INDICATOR_DELAY = 500L
|
||||
|
||||
/**
|
||||
* Load data in an I/O thread. Updates the returned [LiveData] with the current loading state.
|
||||
*
|
||||
* If loading takes longer than [LOADING_INDICATOR_DELAY] the [LoaderState.Loading] state will be emitted so the UI can
|
||||
* display a loading indicator. We use a delay so fast loads won't flash a loading indicator.
|
||||
* If an exception is thrown during loading the [LoaderState.Error] state is emitted.
|
||||
* If the data was loaded successfully [LoaderState.Data] will be emitted containing the data.
|
||||
*/
|
||||
fun <T> liveDataLoader(block: CoroutineScope.() -> T): LiveData<LoaderState<T>> = liveData {
|
||||
coroutineScope {
|
||||
val job = launch {
|
||||
delay(LOADING_INDICATOR_DELAY)
|
||||
|
||||
// Emit loading state if loading took longer than configured delay. If the data was loaded faster than that,
|
||||
// this coroutine will have been canceled before the next line is executed.
|
||||
emit(LoaderState.Loading)
|
||||
}
|
||||
|
||||
val finalState = try {
|
||||
val data = withContext(Dispatchers.IO) {
|
||||
block()
|
||||
}
|
||||
|
||||
LoaderState.Data(data)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error loading data")
|
||||
LoaderState.Error
|
||||
}
|
||||
|
||||
// Cancel job that emits Loading state
|
||||
job.cancelAndJoin()
|
||||
|
||||
emit(finalState)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class LoaderState<out T> {
|
||||
object Loading : LoaderState<Nothing>()
|
||||
object Error : LoaderState<Nothing>()
|
||||
class Data<T>(val data: T) : LoaderState<T>()
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package com.fsck.k9.ui.base.loader
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
|
||||
/**
|
||||
* Use to observe the [LiveData] returned by [liveDataLoader].
|
||||
*
|
||||
* Works with separate views for the [LoaderState.Loading], [LoaderState.Error], and [LoaderState.Data] states. The
|
||||
* view associated with the current state is made visible and the others are hidden. For the [LoaderState.Data] state
|
||||
* the [displayData] function is also called.
|
||||
*/
|
||||
fun <T> LiveData<LoaderState<T>>.observeLoading(
|
||||
owner: LifecycleOwner,
|
||||
loadingView: View,
|
||||
errorView: View,
|
||||
dataView: View,
|
||||
displayData: (T) -> Unit
|
||||
) {
|
||||
observe(owner, LoaderStateObserver(loadingView, errorView, dataView, displayData))
|
||||
}
|
||||
|
||||
private class LoaderStateObserver<T>(
|
||||
private val loadingView: View,
|
||||
private val errorView: View,
|
||||
private val dataView: View,
|
||||
private val displayData: (T) -> Unit
|
||||
) : Observer<LoaderState<T>> {
|
||||
private val allViews = setOf(loadingView, errorView, dataView)
|
||||
|
||||
override fun onChanged(state: LoaderState<T>?) {
|
||||
when (state) {
|
||||
is LoaderState.Loading -> loadingView.show()
|
||||
is LoaderState.Error -> errorView.show()
|
||||
is LoaderState.Data -> {
|
||||
dataView.show()
|
||||
displayData(state.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun View.show() {
|
||||
for (view in allViews) {
|
||||
view.isVisible = view === this
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import com.fsck.k9.ui.endtoend.endToEndUiModule
|
|||
import com.fsck.k9.ui.folders.foldersUiModule
|
||||
import com.fsck.k9.ui.managefolders.manageFoldersUiModule
|
||||
import com.fsck.k9.ui.messagelist.messageListUiModule
|
||||
import com.fsck.k9.ui.messagesource.messageSourceModule
|
||||
import com.fsck.k9.ui.settings.settingsUiModule
|
||||
import com.fsck.k9.ui.uiModule
|
||||
import com.fsck.k9.view.viewModule
|
||||
|
@ -31,5 +32,6 @@ val uiModules = listOf(
|
|||
accountModule,
|
||||
autodiscoveryProvidersXmlModule,
|
||||
viewModule,
|
||||
changelogUiModule
|
||||
changelogUiModule,
|
||||
messageSourceModule
|
||||
)
|
||||
|
|
|
@ -46,6 +46,7 @@ import com.fsck.k9.ui.base.K9Activity
|
|||
import com.fsck.k9.ui.base.Theme
|
||||
import com.fsck.k9.ui.managefolders.ManageFoldersActivity
|
||||
import com.fsck.k9.ui.messagelist.DefaultFolderProvider
|
||||
import com.fsck.k9.ui.messagesource.MessageSourceActivity
|
||||
import com.fsck.k9.ui.messageview.MessageViewFragment
|
||||
import com.fsck.k9.ui.messageview.MessageViewFragment.MessageViewFragmentListener
|
||||
import com.fsck.k9.ui.messageview.PlaceholderFragment
|
||||
|
@ -899,9 +900,8 @@ open class MessageList :
|
|||
} else if (id == R.id.move_to_drafts) {
|
||||
messageViewFragment!!.onMoveToDrafts()
|
||||
return true
|
||||
} else if (id == R.id.show_headers || id == R.id.hide_headers) {
|
||||
messageViewFragment!!.onToggleAllHeadersView()
|
||||
updateMenu()
|
||||
} else if (id == R.id.show_headers) {
|
||||
startActivity(MessageSourceActivity.createLaunchIntent(this, messageViewFragment!!.messageReference))
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -969,7 +969,6 @@ open class MessageList :
|
|||
menu.findItem(R.id.toggle_unread).isVisible = false
|
||||
menu.findItem(R.id.toggle_message_view_theme).isVisible = false
|
||||
menu.findItem(R.id.show_headers).isVisible = false
|
||||
menu.findItem(R.id.hide_headers).isVisible = false
|
||||
} else {
|
||||
// hide prev/next buttons in split mode
|
||||
if (displayMode != DisplayMode.MESSAGE_VIEW) {
|
||||
|
@ -1051,12 +1050,6 @@ open class MessageList :
|
|||
if (messageViewFragment!!.isOutbox) {
|
||||
menu.findItem(R.id.move_to_drafts).isVisible = true
|
||||
}
|
||||
|
||||
if (messageViewFragment!!.allHeadersVisible()) {
|
||||
menu.findItem(R.id.show_headers).isVisible = false
|
||||
} else {
|
||||
menu.findItem(R.id.hide_headers).isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
// Set visibility of menu items related to the message list
|
||||
|
|
|
@ -22,6 +22,6 @@ inline fun FragmentActivity.fragmentTransactionWithBackStack(
|
|||
}
|
||||
}
|
||||
|
||||
fun Fragment.withArguments(vararg argumentPairs: Pair<String, Any?>) = apply {
|
||||
fun <T : Fragment> T.withArguments(vararg argumentPairs: Pair<String, Any?>) = apply {
|
||||
arguments = bundleOf(*argumentPairs)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package com.fsck.k9.ui.messagesource
|
||||
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val messageSourceModule = module {
|
||||
viewModel { MessageHeadersViewModel(messageRepository = get()) }
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package com.fsck.k9.ui.messagesource
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.fsck.k9.controller.MessageReference
|
||||
import com.fsck.k9.mail.Header
|
||||
import com.fsck.k9.mail.internet.MimeUtility
|
||||
import com.fsck.k9.ui.R
|
||||
import com.fsck.k9.ui.base.loader.observeLoading
|
||||
import com.fsck.k9.ui.withArguments
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class MessageHeadersFragment : Fragment() {
|
||||
private val messageHeadersViewModel: MessageHeadersViewModel by viewModel()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.message_view_headers, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val messageReferenceString = requireArguments().getString(ARG_REFERENCE)
|
||||
?: error("Missing argument $ARG_REFERENCE")
|
||||
val messageReference = MessageReference.parse(messageReferenceString)
|
||||
?: error("Invalid message reference: $messageReferenceString")
|
||||
|
||||
val messageHeaderView = view.findViewById<TextView>(R.id.message_source)
|
||||
|
||||
messageHeadersViewModel.loadHeaders(messageReference).observeLoading(
|
||||
owner = this,
|
||||
loadingView = view.findViewById(R.id.message_headers_loading),
|
||||
errorView = view.findViewById(R.id.message_headers_error),
|
||||
dataView = view.findViewById(R.id.message_headers_data)
|
||||
) { headers ->
|
||||
populateHeadersList(messageHeaderView, headers)
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateHeadersList(messageHeaderView: TextView, headers: List<Header>) {
|
||||
val sb = SpannableStringBuilder()
|
||||
var first = true
|
||||
for ((name, value) in headers) {
|
||||
if (!first) {
|
||||
sb.append("\n")
|
||||
} else {
|
||||
first = false
|
||||
}
|
||||
val boldSpan = StyleSpan(Typeface.BOLD)
|
||||
val label = SpannableString("$name: ")
|
||||
label.setSpan(boldSpan, 0, label.length, 0)
|
||||
sb.append(label)
|
||||
sb.append(MimeUtility.unfoldAndDecode(value))
|
||||
}
|
||||
messageHeaderView.text = sb
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_REFERENCE = "reference"
|
||||
|
||||
fun newInstance(reference: MessageReference): MessageHeadersFragment {
|
||||
return MessageHeadersFragment().withArguments(
|
||||
ARG_REFERENCE to reference.toIdentityString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.fsck.k9.ui.messagesource
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.fsck.k9.controller.MessageReference
|
||||
import com.fsck.k9.mail.Header
|
||||
import com.fsck.k9.mailstore.MessageRepository
|
||||
import com.fsck.k9.ui.base.loader.LoaderState
|
||||
import com.fsck.k9.ui.base.loader.liveDataLoader
|
||||
|
||||
private typealias MessageHeaderState = LoaderState<List<Header>>
|
||||
|
||||
class MessageHeadersViewModel(private val messageRepository: MessageRepository) : ViewModel() {
|
||||
private var messageHeaderLiveData: LiveData<MessageHeaderState>? = null
|
||||
|
||||
fun loadHeaders(messageReference: MessageReference): LiveData<MessageHeaderState> {
|
||||
return messageHeaderLiveData ?: loadMessageHeader(messageReference).also { messageHeaderLiveData = it }
|
||||
}
|
||||
|
||||
private fun loadMessageHeader(messageReference: MessageReference): LiveData<MessageHeaderState> {
|
||||
return liveDataLoader {
|
||||
messageRepository.getHeaders(messageReference)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package com.fsck.k9.ui.messagesource
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.fragment.app.commit
|
||||
import com.fsck.k9.controller.MessageReference
|
||||
import com.fsck.k9.ui.R
|
||||
import com.fsck.k9.ui.base.K9Activity
|
||||
|
||||
/**
|
||||
* Temporary Activity used until the fragment can be displayed in the main activity.
|
||||
*/
|
||||
class MessageSourceActivity : K9Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setLayout(R.layout.message_view_headers_activity)
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
addMessageHeadersFragment()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addMessageHeadersFragment() {
|
||||
val messageReferenceString = intent.getStringExtra(ARG_REFERENCE) ?: error("Missing argument $ARG_REFERENCE")
|
||||
val messageReference = MessageReference.parse(messageReferenceString)
|
||||
?: error("Invalid message reference: $messageReferenceString")
|
||||
|
||||
val fragment = MessageHeadersFragment.newInstance(messageReference)
|
||||
supportFragmentManager.commit {
|
||||
add(R.id.message_headers_fragment, fragment)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == android.R.id.home) {
|
||||
finish()
|
||||
true
|
||||
} else {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_REFERENCE = "reference"
|
||||
|
||||
fun createLaunchIntent(context: Context, messageReference: MessageReference): Intent {
|
||||
return Intent(context, MessageSourceActivity::class.java).apply {
|
||||
putExtra(ARG_REFERENCE, messageReference.toIdentityString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -224,14 +224,6 @@ public class MessageTopView extends LinearLayout {
|
|||
mHeaderContainer.setOnMenuItemClickListener(listener);
|
||||
}
|
||||
|
||||
public void showAllHeaders() {
|
||||
mHeaderContainer.onShowAdditionalHeaders();
|
||||
}
|
||||
|
||||
public boolean additionalHeadersVisible() {
|
||||
return mHeaderContainer.additionalHeadersVisible();
|
||||
}
|
||||
|
||||
private void hideHeaderView() {
|
||||
mHeaderContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
|
|
@ -304,14 +304,6 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
|
|||
}
|
||||
}
|
||||
|
||||
public void onToggleAllHeadersView() {
|
||||
mMessageView.getMessageHeaderView().onShowAdditionalHeaders();
|
||||
}
|
||||
|
||||
public boolean allHeadersVisible() {
|
||||
return mMessageView.getMessageHeaderView().additionalHeadersVisible();
|
||||
}
|
||||
|
||||
private void delete() {
|
||||
if (mMessage != null) {
|
||||
// Disable the delete button after it's tapped (to try to prevent
|
||||
|
|
|
@ -68,7 +68,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
|
|||
private View mChip;
|
||||
private CheckBox mFlagged;
|
||||
private int defaultSubjectColor;
|
||||
private TextView mAdditionalHeadersView;
|
||||
private View singleMessageOptionIcon;
|
||||
private View mAnsweredIcon;
|
||||
private View mForwardedIcon;
|
||||
|
@ -76,7 +75,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
|
|||
private Account mAccount;
|
||||
private FontSizes mFontSizes = K9.getFontSizes();
|
||||
private Contacts mContacts;
|
||||
private SavedState mSavedState;
|
||||
|
||||
private MessageHelper mMessageHelper;
|
||||
private ContactPictureLoader mContactsPictureLoader;
|
||||
|
@ -112,7 +110,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
|
|||
singleMessageOptionIcon = findViewById(R.id.icon_single_message_options);
|
||||
|
||||
mSubjectView = findViewById(R.id.subject);
|
||||
mAdditionalHeadersView = findViewById(R.id.additional_headers_view);
|
||||
mChip = findViewById(R.id.chip);
|
||||
mDateView = findViewById(R.id.date);
|
||||
mFlagged = findViewById(R.id.flagged);
|
||||
|
@ -120,7 +117,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
|
|||
defaultSubjectColor = mSubjectView.getCurrentTextColor();
|
||||
mFontSizes.setViewTextSize(mSubjectView, mFontSizes.getMessageViewSubject());
|
||||
mFontSizes.setViewTextSize(mDateView, mFontSizes.getMessageViewDate());
|
||||
mFontSizes.setViewTextSize(mAdditionalHeadersView, mFontSizes.getMessageViewAdditionalHeaders());
|
||||
|
||||
mFontSizes.setViewTextSize(mFromView, mFontSizes.getMessageViewSender());
|
||||
mFontSizes.setViewTextSize(mToView, mFontSizes.getMessageViewTo());
|
||||
|
@ -147,8 +143,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
|
|||
mCryptoStatusIcon.setOnClickListener(this);
|
||||
|
||||
mMessageHelper = MessageHelper.getInstance(mContext);
|
||||
|
||||
hideAdditionalHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -222,51 +216,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
|
|||
mFlagged.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
public boolean additionalHeadersVisible() {
|
||||
return (mAdditionalHeadersView != null &&
|
||||
mAdditionalHeadersView.getVisibility() == View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the text field for the additional headers display if they are
|
||||
* not shown, to save UI resources.
|
||||
*/
|
||||
private void hideAdditionalHeaders() {
|
||||
mAdditionalHeadersView.setVisibility(View.GONE);
|
||||
mAdditionalHeadersView.setText("");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set up and then show the additional headers view. Called by
|
||||
* {@link #onShowAdditionalHeaders()}
|
||||
* (when switching between messages).
|
||||
*/
|
||||
private void showAdditionalHeaders() {
|
||||
Integer messageToShow = null;
|
||||
try {
|
||||
// Retrieve additional headers
|
||||
List<Header> additionalHeaders = mMessage.getHeaders();
|
||||
if (!additionalHeaders.isEmpty()) {
|
||||
// Show the additional headers that we have got.
|
||||
populateAdditionalHeadersView(additionalHeaders);
|
||||
mAdditionalHeadersView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// All headers have been downloaded, but there are no additional headers.
|
||||
messageToShow = R.string.message_no_additional_headers_available;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
messageToShow = R.string.message_additional_headers_retrieval_failed;
|
||||
}
|
||||
// Show a message to the user, if any
|
||||
if (messageToShow != null) {
|
||||
Toast toast = Toast.makeText(mContext, messageToShow, Toast.LENGTH_LONG);
|
||||
toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL, 0, 0);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void populate(final Message message, final Account account, boolean showStar) {
|
||||
Address fromAddress = null;
|
||||
Address[] fromAddresses = message.getFrom();
|
||||
|
@ -334,15 +283,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
|
|||
mChip.setBackgroundColor(mAccount.getChipColor());
|
||||
|
||||
setVisibility(View.VISIBLE);
|
||||
|
||||
if (mSavedState != null) {
|
||||
if (mSavedState.additionalHeadersVisible) {
|
||||
showAdditionalHeaders();
|
||||
}
|
||||
mSavedState = null;
|
||||
} else {
|
||||
hideAdditionalHeaders();
|
||||
}
|
||||
}
|
||||
|
||||
public void setSubject(@NonNull String subject) {
|
||||
|
@ -381,20 +321,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
|
|||
mCryptoStatusIcon.setColorFilter(color);
|
||||
}
|
||||
|
||||
public void onShowAdditionalHeaders() {
|
||||
int currentVisibility = mAdditionalHeadersView.getVisibility();
|
||||
if (currentVisibility == View.VISIBLE) {
|
||||
hideAdditionalHeaders();
|
||||
expand(mToView, false);
|
||||
expand(mCcView, false);
|
||||
} else {
|
||||
showAdditionalHeaders();
|
||||
expand(mToView, true);
|
||||
expand(mCcView, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void updateAddressField(TextView v, CharSequence text, View label) {
|
||||
boolean hasText = !TextUtils.isEmpty(text);
|
||||
|
||||
|
@ -416,91 +342,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the additional headers text view with the supplied header data.
|
||||
*
|
||||
* @param additionalHeaders List of header entries. Each entry consists of a header
|
||||
* name and a header value. Header names may appear multiple
|
||||
* times.
|
||||
* <p/>
|
||||
* This method is always called from within the UI thread by
|
||||
* {@link #showAdditionalHeaders()}.
|
||||
*/
|
||||
private void populateAdditionalHeadersView(final List<Header> additionalHeaders) {
|
||||
SpannableStringBuilder sb = new SpannableStringBuilder();
|
||||
boolean first = true;
|
||||
for (Header additionalHeader : additionalHeaders) {
|
||||
if (!first) {
|
||||
sb.append("\n");
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
|
||||
SpannableString label = new SpannableString(additionalHeader.getName() + ": ");
|
||||
label.setSpan(boldSpan, 0, label.length(), 0);
|
||||
sb.append(label);
|
||||
sb.append(MimeUtility.unfoldAndDecode(additionalHeader.getValue()));
|
||||
}
|
||||
mAdditionalHeadersView.setText(sb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
Parcelable superState = super.onSaveInstanceState();
|
||||
|
||||
SavedState savedState = new SavedState(superState);
|
||||
|
||||
savedState.additionalHeadersVisible = additionalHeadersVisible();
|
||||
|
||||
return savedState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Parcelable state) {
|
||||
if(!(state instanceof SavedState)) {
|
||||
super.onRestoreInstanceState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
SavedState savedState = (SavedState)state;
|
||||
super.onRestoreInstanceState(savedState.getSuperState());
|
||||
|
||||
mSavedState = savedState;
|
||||
}
|
||||
|
||||
static class SavedState extends BaseSavedState {
|
||||
boolean additionalHeadersVisible;
|
||||
|
||||
public static final Parcelable.Creator<SavedState> CREATOR =
|
||||
new Parcelable.Creator<SavedState>() {
|
||||
@Override
|
||||
public SavedState createFromParcel(Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedState[] newArray(int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
SavedState(Parcelable superState) {
|
||||
super(superState);
|
||||
}
|
||||
|
||||
private SavedState(Parcel in) {
|
||||
super(in);
|
||||
this.additionalHeadersVisible = (in.readInt() != 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeInt((this.additionalHeadersVisible) ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnCryptoClickListener(OnCryptoClickListener onCryptoClickListener) {
|
||||
this.onCryptoClickListener = onCryptoClickListener;
|
||||
}
|
||||
|
|
|
@ -277,18 +277,6 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/additional_headers_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginRight="6dip"
|
||||
android:singleLine="false"
|
||||
android:ellipsize="none"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textIsSelectable="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
47
app/ui/legacy/src/main/res/layout/message_view_headers.xml
Normal file
47
app/ui/legacy/src/main/res/layout/message_view_headers.xml
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/message_headers_data"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_source"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="none"
|
||||
android:padding="16dp"
|
||||
android:singleLine="false"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="true"
|
||||
tools:text="From: alice@domain.example\nTo: bob@domain.example\nSubject: Hi Bob" />
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/message_headers_loading"
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_headers_error"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/generic_loading_error"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<include layout="@layout/toolbar"/>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/message_headers_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
|
@ -150,10 +150,6 @@
|
|||
app:showAsAction="never"
|
||||
android:title="@string/show_headers_action"/>
|
||||
|
||||
<item android:id="@+id/hide_headers"
|
||||
app:showAsAction="never"
|
||||
android:title="@string/hide_headers_action"/>
|
||||
|
||||
<!-- always -->
|
||||
<item
|
||||
android:id="@+id/compose"
|
||||
|
|
|
@ -150,7 +150,6 @@ Please submit bug reports, contribute new features and ask questions at
|
|||
<string name="unflag_action">Remove star</string>
|
||||
<string name="copy_action">Copy</string>
|
||||
<string name="show_headers_action">Show headers</string>
|
||||
<string name="hide_headers_action">Hide headers</string>
|
||||
<plurals name="copy_address_to_clipboard">
|
||||
<item quantity="one">Address copied to clipboard</item>
|
||||
<item quantity="other">Addresses copied to clipboard</item>
|
||||
|
@ -1261,4 +1260,6 @@ You can keep this message and use it as a backup for your secret key. If you wan
|
|||
<!-- permissions -->
|
||||
<string name="permission_contacts_rationale_title">Allow access to contacts</string>
|
||||
<string name="permission_contacts_rationale_message">To be able to provide contact suggestions and to display contact names and photos, the app needs access to your contacts.</string>
|
||||
|
||||
<string name="generic_loading_error">An error occurred while loading the data</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
package com.fsck.k9.mail.message;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.Part;
|
||||
import org.apache.james.mime4j.MimeException;
|
||||
import org.apache.james.mime4j.parser.ContentHandler;
|
||||
import org.apache.james.mime4j.parser.MimeStreamParser;
|
||||
import org.apache.james.mime4j.stream.BodyDescriptor;
|
||||
import org.apache.james.mime4j.stream.Field;
|
||||
import org.apache.james.mime4j.stream.MimeConfig;
|
||||
|
||||
|
||||
public class MessageHeaderParser {
|
||||
|
||||
public static void parse(final Part part, InputStream headerInputStream) throws MessagingException {
|
||||
MimeStreamParser parser = getMimeStreamParser();
|
||||
parser.setContentHandler(new MessageHeaderParserContentHandler(part));
|
||||
|
||||
try {
|
||||
parser.parse(headerInputStream);
|
||||
} catch (MimeException me) {
|
||||
throw new MessagingException("Error parsing headers", me);
|
||||
} catch (IOException e) {
|
||||
throw new MessagingException("I/O error parsing headers", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static MimeStreamParser getMimeStreamParser() {
|
||||
MimeConfig parserConfig = new MimeConfig.Builder()
|
||||
.setMaxHeaderLen(-1)
|
||||
.setMaxLineLen(-1)
|
||||
.setMaxHeaderCount(-1)
|
||||
.build();
|
||||
|
||||
return new MimeStreamParser(parserConfig);
|
||||
}
|
||||
|
||||
private static class MessageHeaderParserContentHandler implements ContentHandler {
|
||||
private final Part part;
|
||||
|
||||
public MessageHeaderParserContentHandler(Part part) {
|
||||
this.part = part;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void field(Field rawField) throws MimeException {
|
||||
String name = rawField.getName();
|
||||
String raw = rawField.getRaw().toString();
|
||||
part.addRawHeader(name, raw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startMessage() throws MimeException {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endMessage() throws MimeException {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startBodyPart() throws MimeException {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endBodyPart() throws MimeException {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startHeader() throws MimeException {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endHeader() throws MimeException {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preamble(InputStream is) throws MimeException, IOException {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void epilogue(InputStream is) throws MimeException, IOException {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startMultipart(BodyDescriptor bd) throws MimeException {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endMultipart() throws MimeException {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void body(BodyDescriptor bd, InputStream is) throws MimeException, IOException {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void raw(InputStream is) throws MimeException, IOException {
|
||||
/* do nothing */
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package com.fsck.k9.mail.message
|
||||
|
||||
import com.fsck.k9.mail.MessagingException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import org.apache.james.mime4j.MimeException
|
||||
import org.apache.james.mime4j.parser.AbstractContentHandler
|
||||
import org.apache.james.mime4j.parser.MimeStreamParser
|
||||
import org.apache.james.mime4j.stream.Field
|
||||
import org.apache.james.mime4j.stream.MimeConfig
|
||||
|
||||
object MessageHeaderParser {
|
||||
@Throws(MessagingException::class)
|
||||
@JvmStatic
|
||||
fun parse(headerInputStream: InputStream, collector: MessageHeaderCollector) {
|
||||
val parser = createMimeStreamParser().apply {
|
||||
setContentHandler(MessageHeaderParserContentHandler(collector))
|
||||
}
|
||||
|
||||
try {
|
||||
parser.parse(headerInputStream)
|
||||
} catch (me: MimeException) {
|
||||
throw MessagingException("Error parsing headers", me)
|
||||
} catch (e: IOException) {
|
||||
throw MessagingException("I/O error parsing headers", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMimeStreamParser(): MimeStreamParser {
|
||||
val parserConfig = MimeConfig.Builder()
|
||||
.setMaxHeaderLen(-1)
|
||||
.setMaxLineLen(-1)
|
||||
.setMaxHeaderCount(-1)
|
||||
.build()
|
||||
|
||||
return MimeStreamParser(parserConfig)
|
||||
}
|
||||
|
||||
private class MessageHeaderParserContentHandler(
|
||||
private val collector: MessageHeaderCollector
|
||||
) : AbstractContentHandler() {
|
||||
override fun field(rawField: Field) {
|
||||
val name = rawField.name
|
||||
val raw = rawField.raw.toString()
|
||||
collector.addRawHeader(name, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun interface MessageHeaderCollector {
|
||||
fun addRawHeader(name: String, raw: String)
|
||||
}
|
Loading…
Reference in a new issue