messageview: move all loading logic into MessageLoaderHelper (breaks MessageCompose)

This commit is contained in:
Vincent Breitmoser 2016-05-30 17:18:12 +02:00
parent 0df44a1457
commit b72dba67df
9 changed files with 575 additions and 345 deletions

View file

@ -1954,21 +1954,19 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
@Override
public void loadMessageForViewFinished(Account account, String folder, String uid, LocalMessage message) {
public void loadMessageForViewFinished(Account account, String folder, String uid) {
if (mMessageReference == null || !mMessageReference.getUid().equals(uid)) {
return;
}
mHandler.sendEmptyMessage(MSG_PROGRESS_OFF);
}
@Override
public void loadMessageForViewBodyAvailable(Account account, String folder, String uid,
final LocalMessage message) {
if (mMessageReference == null || !mMessageReference.getUid().equals(uid)) {
return;
}
final LocalMessage message = null; // TODO this isn't working at the moment!
runOnUiThread(new Runnable() {
@Override
public void run() {

View file

@ -0,0 +1,425 @@
package com.fsck.k9.activity;
import android.app.FragmentManager;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.Loader;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.util.Log;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.helper.RetainFragment;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.MessageViewInfo;
import com.fsck.k9.ui.crypto.MessageCryptoAnnotations;
import com.fsck.k9.ui.crypto.MessageCryptoCallback;
import com.fsck.k9.ui.crypto.MessageCryptoHelper;
import com.fsck.k9.ui.message.LocalMessageExtractorLoader;
import com.fsck.k9.ui.message.LocalMessageLoader;
/** This class is responsible for loading a message start to finish, and
* retaining or reloading the loading state on configuration changes.
*
* In particular, it takes care of the following:
* - load raw message data from the database, using LocalMessageLoader
* - download partial message content if it is missing using MessagingController
* - apply crypto operations if applicable, using MessageCryptoHelper
* - extract MessageViewInfo from the message and crypto data using DecodeMessageLoader
* - download complete message content for partially downloaded messages if requested
*
* No state is retained in this object itself. Instead, state is stored in the
* message loaders and the MessageCryptoHelper which is stored in a
* RetainFragment. The public interface is intended for use by an Activity or
* Fragment, which should construct a new instance of this class in onCreate,
* then call asyncStartOrResumeLoadingMessage to start or resume loading the
* message, receiving callbacks when it is loaded.
*
* When the Activity or Fragment is ultimately destroyed, it should call
* onDestroy, which stops loading and deletes all state kept in loaders and
* fragments by this object. If it is only destroyed for a configuration
* change, it should call onDestroyChangingConfigurations, which cancels any
* further callbacks from this object but retains the loading state to resume
* from at the next call to asyncStartOrResumeLoadingMessage.
*
* If the message is already loaded, a call to asyncStartOrResumeLoadingMessage
* will typically load by starting the decode message loader, retrieving the
* already cached LocalMessage. This message will be passed to the retained
* CryptoMessageHelper instance, returning the already cached
* MessageCryptoAnnotations. These two objects will be checked against the
* retained DecodeMessageLoader, returning the final result. At each
* intermediate step, the input of the respective loaders will be checked for
* consistency, reloading if there is a mismatch.
*
*/
public class MessageLoaderHelper {
private static final int LOCAL_MESSAGE_LOADER_ID = 1;
private static final int DECODE_MESSAGE_LOADER_ID = 2;
// injected state
private final Context context;
private final FragmentManager fragmentManager;
private final LoaderManager loaderManager;
@Nullable // may be cleared
private MessageLoaderCallbacks callback;
// transient state
private MessageReference messageReference;
private Account account;
private LocalMessage localMessage;
private MessageCryptoAnnotations messageCryptoAnnotations;
private MessageCryptoHelper messageCryptoHelper;
public MessageLoaderHelper(Context context, LoaderManager loaderManager, FragmentManager fragmentManager,
@NonNull MessageLoaderCallbacks callback) {
this.context = context;
this.loaderManager = loaderManager;
this.fragmentManager = fragmentManager;
this.callback = callback;
}
// public interface
@UiThread
public void asyncStartOrResumeLoadingMessage(MessageReference messageReference) {
this.messageReference = messageReference;
this.account = Preferences.getPreferences(context).getAccount(messageReference.getAccountUuid());
startOrResumeLocalMessageLoader();
}
/** Cancels all loading processes, prevents future callbacks, and destroys all loading state. */
@UiThread
public void onDestroy() {
if (messageCryptoHelper != null) {
messageCryptoHelper.cancelIfRunning();
}
callback = null;
}
/** Prevents future callbacks, but retains loading state to pick up from in a call to
* asyncStartOrResumeLoadingMessage in a new instance of this class. */
@UiThread
public void onDestroyChangingConfigurations() {
if (messageCryptoHelper != null) {
messageCryptoHelper.detachCallback();
}
callback = null;
}
@UiThread
public void downloadCompleteMessage() {
if (localMessage.isSet(Flag.X_DOWNLOADED_FULL)) {
return;
}
startDownloadingMessageBody(true);
}
@UiThread
public void restartMessageCryptoProcessing() {
cancelAndClearCryptoOperation();
cancelAndClearDecodeLoader();
startOrResumeCryptoOperation();
}
@UiThread
public void onActivityResult(int requestCode, int resultCode, Intent data) {
messageCryptoHelper.onActivityResult(requestCode, resultCode, data);
}
// load from database
private void startOrResumeLocalMessageLoader() {
LocalMessageLoader loader =
(LocalMessageLoader) loaderManager.<LocalMessage>getLoader(LOCAL_MESSAGE_LOADER_ID);
boolean isLoaderStale = (loader == null) || !loader.isCreatedFor(messageReference);
if (isLoaderStale) {
Log.d(K9.LOG_TAG, "Creating new local message loader");
cancelAndClearCryptoOperation();
cancelAndClearDecodeLoader();
loaderManager.restartLoader(LOCAL_MESSAGE_LOADER_ID, null, localMessageLoaderCallback);
} else {
Log.d(K9.LOG_TAG, "Reusing local message loader");
loaderManager.initLoader(LOCAL_MESSAGE_LOADER_ID, null, localMessageLoaderCallback);
}
}
@UiThread
private void onLoadMessageFromDatabaseFinished() {
if (callback == null) {
throw new IllegalStateException("unexpected call when callback is already detached");
}
callback.onMessageDataLoadFinished(localMessage);
if (localMessage.isBodyMissing()) {
startDownloadingMessageBody(false);
return;
}
if (account.isOpenPgpProviderConfigured()) {
startOrResumeCryptoOperation();
return;
}
startOrResumeDecodeMessage();
}
private void onLoadMessageFromDatabaseFailed() {
if (callback == null) {
throw new IllegalStateException("unexpected call when callback is already detached");
}
callback.onMessageDataLoadFailed();
}
private void cancelAndClearLocalMessageLoader() {
loaderManager.destroyLoader(LOCAL_MESSAGE_LOADER_ID);
}
private LoaderCallbacks<LocalMessage> localMessageLoaderCallback = new LoaderCallbacks<LocalMessage>() {
@Override
public Loader<LocalMessage> onCreateLoader(int id, Bundle args) {
if (id != LOCAL_MESSAGE_LOADER_ID) {
throw new IllegalStateException("loader id must be message loader id");
}
return new LocalMessageLoader(context, MessagingController.getInstance(context), account, messageReference);
}
@Override
public void onLoadFinished(Loader<LocalMessage> loader, LocalMessage message) {
if (loader.getId() != LOCAL_MESSAGE_LOADER_ID) {
throw new IllegalStateException("loader id must be message loader id");
}
localMessage = message;
if (message == null) {
onLoadMessageFromDatabaseFailed();
} else {
onLoadMessageFromDatabaseFinished();
}
}
@Override
public void onLoaderReset(Loader<LocalMessage> loader) {
if (loader.getId() != LOCAL_MESSAGE_LOADER_ID) {
throw new IllegalStateException("loader id must be message loader id");
}
// Do nothing
}
};
// process with crypto helper
private void startOrResumeCryptoOperation() {
RetainFragment<MessageCryptoHelper> retainCryptoHelperFragment = getMessageCryptoHelperRetainFragment();
if (retainCryptoHelperFragment.hasData()) {
messageCryptoHelper = retainCryptoHelperFragment.getData();
} else {
messageCryptoHelper = new MessageCryptoHelper(context, account.getOpenPgpProvider());
retainCryptoHelperFragment.setData(messageCryptoHelper);
}
messageCryptoHelper.asyncStartOrResumeProcessingMessage(localMessage, messageCryptoCallback);
}
private void cancelAndClearCryptoOperation() {
RetainFragment<MessageCryptoHelper> retainCryptoHelperFragment = getMessageCryptoHelperRetainFragment();
if (retainCryptoHelperFragment != null) {
if (retainCryptoHelperFragment.hasData()) {
messageCryptoHelper = retainCryptoHelperFragment.getData();
messageCryptoHelper.cancelIfRunning();
messageCryptoHelper = null;
}
retainCryptoHelperFragment.clearAndRemove(fragmentManager);
}
}
private RetainFragment<MessageCryptoHelper> getMessageCryptoHelperRetainFragment() {
return RetainFragment.findOrCreate(fragmentManager, "crypto_helper_" + messageReference.hashCode());
}
private MessageCryptoCallback messageCryptoCallback = new MessageCryptoCallback() {
@Override
public void onCryptoHelperProgress(int current, int max) {
if (callback == null) {
throw new IllegalStateException("unexpected call when callback is already detached");
}
callback.setLoadingProgress(current, max);
}
@Override
public void onCryptoOperationsFinished(MessageCryptoAnnotations annotations) {
if (callback == null) {
throw new IllegalStateException("unexpected call when callback is already detached");
}
messageCryptoAnnotations = annotations;
startOrResumeDecodeMessage();
}
@Override
public void startPendingIntentForCryptoHelper(IntentSender si, int requestCode, Intent fillIntent,
int flagsMask, int flagValues, int extraFlags) {
if (callback == null) {
throw new IllegalStateException("unexpected call when callback is already detached");
}
callback.startIntentSenderForMessageLoaderHelper(si, requestCode, fillIntent,
flagsMask, flagValues, extraFlags);
}
};
// decode message
private void startOrResumeDecodeMessage() {
LocalMessageExtractorLoader loader =
(LocalMessageExtractorLoader) loaderManager.<MessageViewInfo>getLoader(DECODE_MESSAGE_LOADER_ID);
boolean isLoaderStale = (loader == null) || !loader.isCreatedFor(localMessage, messageCryptoAnnotations);
if (isLoaderStale) {
Log.d(K9.LOG_TAG, "Creating new decode message loader");
loaderManager.restartLoader(DECODE_MESSAGE_LOADER_ID, null, decodeMessageLoaderCallback);
} else {
Log.d(K9.LOG_TAG, "Reusing decode message loader");
loaderManager.initLoader(DECODE_MESSAGE_LOADER_ID, null, decodeMessageLoaderCallback);
}
}
private void onDecodeMessageFinished(MessageViewInfo messageViewInfo) {
if (callback == null) {
throw new IllegalStateException("unexpected call when callback is already detached");
}
if (messageViewInfo == null) {
callback.onMessageViewInfoLoadFailed(localMessage);
return;
}
callback.onMessageViewInfoLoadFinished(localMessage, messageViewInfo);
}
private void cancelAndClearDecodeLoader() {
loaderManager.destroyLoader(DECODE_MESSAGE_LOADER_ID);
}
private LoaderCallbacks<MessageViewInfo> decodeMessageLoaderCallback = new LoaderCallbacks<MessageViewInfo>() {
@Override
public Loader<MessageViewInfo> onCreateLoader(int id, Bundle args) {
if (id != DECODE_MESSAGE_LOADER_ID) {
throw new IllegalStateException("loader id must be message decoder id");
}
return new LocalMessageExtractorLoader(context, localMessage, messageCryptoAnnotations);
}
@Override
public void onLoadFinished(Loader<MessageViewInfo> loader, MessageViewInfo messageViewInfo) {
if (loader.getId() != DECODE_MESSAGE_LOADER_ID) {
throw new IllegalStateException("loader id must be message decoder id");
}
onDecodeMessageFinished(messageViewInfo);
}
@Override
public void onLoaderReset(Loader<MessageViewInfo> loader) {
if (loader.getId() != DECODE_MESSAGE_LOADER_ID) {
throw new IllegalStateException("loader id must be message decoder id");
}
// Do nothing
}
};
// download missing body
private void startDownloadingMessageBody(boolean downloadComplete) {
if (downloadComplete) {
MessagingController.getInstance(context).loadMessageRemote(
account, messageReference.getFolderName(), messageReference.getUid(), downloadMessageListener);
} else {
MessagingController.getInstance(context).loadMessageRemotePartial(
account, messageReference.getFolderName(), messageReference.getUid(), downloadMessageListener);
}
}
private void onMessageDownloadFinished() {
if (callback == null) {
return;
}
cancelAndClearLocalMessageLoader();
cancelAndClearDecodeLoader();
cancelAndClearCryptoOperation();
startOrResumeLocalMessageLoader();
}
private void onDownloadMessageFailed(final Throwable t) {
if (callback == null) {
return;
}
if (t instanceof IllegalArgumentException) {
callback.onDownloadErrorMessageNotFound();
} else {
callback.onDownloadErrorNetworkError();
}
}
MessagingListener downloadMessageListener = new MessagingListener() {
@Override
public void loadMessageRemoteFinished(Account account, String folder, String uid) {
onMessageDownloadFinished();
}
@Override
public void loadMessageRemoteFailed(Account account, String folder, String uid, final Throwable t) {
onDownloadMessageFailed(t);
}
};
// callback interface
public interface MessageLoaderCallbacks {
void onMessageDataLoadFinished(LocalMessage message);
void onMessageDataLoadFailed();
void onMessageViewInfoLoadFinished(LocalMessage localMessage, MessageViewInfo messageViewInfo);
void onMessageViewInfoLoadFailed(LocalMessage localMessage);
void setLoadingProgress(int current, int max);
void startIntentSenderForMessageLoaderHelper(IntentSender si, int requestCode, Intent fillIntent, int flagsMask,
int flagValues, int extraFlags);
void onDownloadErrorMessageNotFound();
void onDownloadErrorNetworkError();
}
}

View file

@ -2696,28 +2696,28 @@ public class MessagingController implements Runnable {
}
}
public void loadMessagePartialForViewRemote(final Account account, final String folder,
public void loadMessageRemotePartial(final Account account, final String folder,
final String uid, final MessagingListener listener) {
put("loadMessageForViewRemote", listener, new Runnable() {
put("loadMessageRemotePartial", listener, new Runnable() {
@Override
public void run() {
loadMessageForViewRemoteSynchronous(account, folder, uid, listener, true);
loadMessageRemoteSynchronous(account, folder, uid, listener, true);
}
});
}
//TODO: Fix the callback mess. See GH-782
public void loadMessageForViewRemote(final Account account, final String folder,
public void loadMessageRemote(final Account account, final String folder,
final String uid, final MessagingListener listener) {
put("loadMessageForViewRemote", listener, new Runnable() {
put("loadMessageRemote", listener, new Runnable() {
@Override
public void run() {
loadMessageForViewRemoteSynchronous(account, folder, uid, listener, false);
loadMessageRemoteSynchronous(account, folder, uid, listener, false);
}
});
}
public boolean loadMessageForViewRemoteSynchronous(final Account account, final String folder,
public boolean loadMessageRemoteSynchronous(final Account account, final String folder,
final String uid, final MessagingListener listener, final boolean loadPartialFromSearch) {
Folder remoteFolder = null;
LocalFolder localFolder = null;
@ -2750,16 +2750,7 @@ public class MessagingController implements Runnable {
message.setFlag(Flag.X_DOWNLOADED_PARTIAL, false);
}*/
if (message.isSet(Flag.X_DOWNLOADED_FULL)) {
/*
* If the message has been synchronized since we were called we'll
* just hand it back cause it's ready to go.
*/
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.BODY);
localFolder.fetch(Collections.singletonList(message), fp, null);
} else {
if (!message.isSet(Flag.X_DOWNLOADED_FULL)) {
/*
* At this point the message is not available, so we need to download it
* fully if possible.
@ -2784,36 +2775,25 @@ public class MessagingController implements Runnable {
message = localFolder.getMessage(uid);
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.BODY);
localFolder.fetch(Collections.singletonList(message), fp, null);
if (!loadPartialFromSearch) {
message.setFlag(Flag.X_DOWNLOADED_FULL, true);
}
}
// Mark that this message is now fully synched
if (account.isMarkMessageAsReadOnView()) {
message.setFlag(Flag.SEEN, true);
}
// Mark that this message is now fully synched
if (account.isMarkMessageAsReadOnView()) {
message.setFlag(Flag.SEEN, true);
}
// now that we have the full message, refresh the headers
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewHeadersAvailable(account, folder, uid, message);
l.loadMessageRemoteFinished(account, folder, uid);
}
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewBodyAvailable(account, folder, uid, message);
}
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewFinished(account, folder, uid, message);
}
return true;
} catch (Exception e) {
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewFailed(account, folder, uid, e);
l.loadMessageRemoteFailed(account, folder, uid, e);
}
notifyUserIfCertificateProblem(account, e, true);
addErrorMessage(account, null, e);
@ -2847,17 +2827,12 @@ public class MessagingController implements Runnable {
// TODO: limit by account.getMaximumAutoDownloadMessageSize().
if (!message.isSet(Flag.X_DOWNLOADED_FULL) &&
!message.isSet(Flag.X_DOWNLOADED_PARTIAL)) {
if (loadMessageForViewRemoteSynchronous(account, folder, uid, listener, true)) {
if (loadMessageRemoteSynchronous(account, folder, uid, listener, true)) {
markMessageAsReadOnView(account, message);
}
return;
}
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewHeadersAvailable(account, folder, uid, message);
}
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.BODY);
@ -2865,11 +2840,7 @@ public class MessagingController implements Runnable {
localFolder.close();
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewBodyAvailable(account, folder, uid, message);
}
for (MessagingListener l : getListeners(listener)) {
l.loadMessageForViewFinished(account, folder, uid, message);
l.loadMessageForViewFinished(account, folder, uid);
}
markMessageAsReadOnView(account, message);
@ -3876,64 +3847,52 @@ public class MessagingController implements Runnable {
return (account.getRemoteStore() instanceof Pop3Store);
}
public void sendAlternate(final Context context, Account account, Message message) {
public void sendAlternate(Context context, Account account, LocalMessage message) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "About to load message " + account.getDescription() + ":" + message.getFolder().getName()
Log.d(K9.LOG_TAG, "Got message " + account.getDescription() + ":" + message.getFolder()
+ ":" + message.getUid() + " for sendAlternate");
loadMessageForView(account, message.getFolder().getName(),
message.getUid(), new MessagingListener() {
@Override
public void loadMessageForViewBodyAvailable(Account account, String folder, String uid,
LocalMessage message) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Got message " + account.getDescription() + ":" + folder
+ ":" + message.getUid() + " for sendAlternate");
try {
Intent msg = new Intent(Intent.ACTION_SEND);
String quotedText = null;
Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain");
if (part == null) {
part = MimeUtility.findFirstPartByMimeType(message, "text/html");
}
if (part != null) {
quotedText = MessageExtractor.getTextFromPart(part);
}
if (quotedText != null) {
msg.putExtra(Intent.EXTRA_TEXT, quotedText);
}
msg.putExtra(Intent.EXTRA_SUBJECT, message.getSubject());
Address[] from = message.getFrom();
String[] senders = new String[from.length];
for (int i = 0; i < from.length; i++) {
senders[i] = from[i].toString();
}
msg.putExtra(Intents.Share.EXTRA_FROM, senders);
Address[] to = message.getRecipients(RecipientType.TO);
String[] recipientsTo = new String[to.length];
for (int i = 0; i < to.length; i++) {
recipientsTo[i] = to[i].toString();
}
msg.putExtra(Intent.EXTRA_EMAIL, recipientsTo);
Address[] cc = message.getRecipients(RecipientType.CC);
String[] recipientsCc = new String[cc.length];
for (int i = 0; i < cc.length; i++) {
recipientsCc[i] = cc[i].toString();
}
msg.putExtra(Intent.EXTRA_CC, recipientsCc);
msg.setType("text/plain");
context.startActivity(Intent.createChooser(msg, context.getString(R.string.send_alternate_chooser_title)));
} catch (MessagingException me) {
Log.e(K9.LOG_TAG, "Unable to send email through alternate program", me);
}
try {
Intent msg = new Intent(Intent.ACTION_SEND);
String quotedText = null;
Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain");
if (part == null) {
part = MimeUtility.findFirstPartByMimeType(message, "text/html");
}
});
if (part != null) {
quotedText = MessageExtractor.getTextFromPart(part);
}
if (quotedText != null) {
msg.putExtra(Intent.EXTRA_TEXT, quotedText);
}
msg.putExtra(Intent.EXTRA_SUBJECT, message.getSubject());
Address[] from = message.getFrom();
String[] senders = new String[from.length];
for (int i = 0; i < from.length; i++) {
senders[i] = from[i].toString();
}
msg.putExtra(Intents.Share.EXTRA_FROM, senders);
Address[] to = message.getRecipients(RecipientType.TO);
String[] recipientsTo = new String[to.length];
for (int i = 0; i < to.length; i++) {
recipientsTo[i] = to[i].toString();
}
msg.putExtra(Intent.EXTRA_EMAIL, recipientsTo);
Address[] cc = message.getRecipients(RecipientType.CC);
String[] recipientsCc = new String[cc.length];
for (int i = 0; i < cc.length; i++) {
recipientsCc[i] = cc[i].toString();
}
msg.putExtra(Intent.EXTRA_CC, recipientsCc);
msg.setType("text/plain");
context.startActivity(Intent.createChooser(msg, context.getString(R.string.send_alternate_chooser_title)));
} catch (MessagingException me) {
Log.e(K9.LOG_TAG, "Unable to send email through alternate program", me);
}
}
/**

View file

@ -81,27 +81,11 @@ public class MessagingListener {
public void synchronizeMailboxFailed(Account account, String folder, String message) {}
public void loadMessageRemoteFinished(Account account, String folder, String uid) {}
public void loadMessageForViewStarted(Account account, String folder, String uid) {}
public void loadMessageForViewHeadersAvailable(Account account, String folder, String uid,
Message message) {}
public void loadMessageForViewBodyAvailable(Account account, String folder, String uid,
LocalMessage message) {}
public void loadMessageForViewFinished(Account account, String folder, String uid,
LocalMessage message) {}
public void loadMessageForViewFailed(Account account, String folder, String uid,
public void loadMessageRemoteFailed(Account account, String folder, String uid,
Throwable t) {}
/**
* Called when a message for view has been fully displayed on the screen.
*/
public void messageViewFinished() {}
public void checkMailStarted(Context context, Account account) {}
public void checkMailFinished(Context context, Account account) {}

View file

@ -280,6 +280,7 @@ public class MessageCryptoHelper {
}
public void cancelIfRunning() {
detachCallback();
isCancelled = true;
if (cancelableBackgroundOperation != null) {
cancelableBackgroundOperation.cancelOperation();
@ -584,6 +585,9 @@ public class MessageCryptoHelper {
throw new AssertionError("Callback may only be reattached for the same message!");
}
synchronized (callbackLock) {
if (queuedResult != null) {
Log.d(K9.LOG_TAG, "Returning cached result to reattached callback");
}
this.callback = callback;
deliverResult();
}

View file

@ -3,10 +3,12 @@ package com.fsck.k9.ui.message;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.MessageViewInfoExtractor;
import com.fsck.k9.mailstore.MessageViewInfo;
import com.fsck.k9.ui.crypto.MessageCryptoAnnotations;
@ -15,9 +17,11 @@ import com.fsck.k9.ui.crypto.MessageCryptoAnnotations;
public class LocalMessageExtractorLoader extends AsyncTaskLoader<MessageViewInfo> {
private final Message message;
private MessageViewInfo messageViewInfo;
@Nullable
private MessageCryptoAnnotations annotations;
public LocalMessageExtractorLoader(Context context, Message message, MessageCryptoAnnotations annotations) {
public LocalMessageExtractorLoader(
Context context, Message message, @Nullable MessageCryptoAnnotations annotations) {
super(context);
this.message = message;
this.annotations = annotations;
@ -49,4 +53,8 @@ public class LocalMessageExtractorLoader extends AsyncTaskLoader<MessageViewInfo
return null;
}
}
public boolean isCreatedFor(LocalMessage localMessage, MessageCryptoAnnotations messageCryptoAnnotations) {
return annotations == messageCryptoAnnotations && message.equals(localMessage);
}
}

View file

@ -57,4 +57,8 @@ public class LocalMessageLoader extends AsyncTaskLoader<LocalMessage> {
private LocalMessage loadMessageFromDatabase() throws MessagingException {
return controller.loadMessage(account, messageReference.getFolderName(), messageReference.getUid());
}
public boolean isCreatedFor(MessageReference messageReference) {
return this.messageReference.equals(messageReference);
}
}

View file

@ -9,17 +9,13 @@ import android.app.DialogFragment;
import android.app.DownloadManager;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.content.Loader;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.UiThread;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextThemeWrapper;
@ -36,25 +32,20 @@ import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.activity.ChooseFolder;
import com.fsck.k9.activity.MessageLoaderHelper;
import com.fsck.k9.activity.MessageLoaderHelper.MessageLoaderCallbacks;
import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.fragment.ConfirmationDialogFragment;
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
import com.fsck.k9.fragment.ProgressDialogFragment;
import com.fsck.k9.helper.FileBrowserHelper;
import com.fsck.k9.helper.FileBrowserHelper.FileBrowserFailOverCallback;
import com.fsck.k9.helper.RetainFragment;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mailstore.AttachmentViewInfo;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.MessageViewInfo;
import com.fsck.k9.ui.crypto.MessageCryptoAnnotations;
import com.fsck.k9.ui.crypto.MessageCryptoCallback;
import com.fsck.k9.ui.crypto.MessageCryptoHelper;
import com.fsck.k9.ui.message.LocalMessageExtractorLoader;
import com.fsck.k9.ui.message.LocalMessageLoader;
import com.fsck.k9.ui.messageview.CryptoInfoDialog.OnClickShowCryptoKeyListener;
import com.fsck.k9.ui.messageview.MessageCryptoPresenter.MessageCryptoMvpView;
import com.fsck.k9.view.MessageCryptoDisplayStatus;
@ -62,7 +53,7 @@ import com.fsck.k9.view.MessageHeader;
public class MessageViewFragment extends Fragment implements ConfirmationDialogFragmentListener,
AttachmentViewCallback, MessageCryptoCallback, MessageCryptoMvpView, OnClickShowCryptoKeyListener {
AttachmentViewCallback, MessageCryptoMvpView, OnClickShowCryptoKeyListener {
private static final String ARG_REFERENCE = "reference";
@ -70,12 +61,8 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2;
private static final int ACTIVITY_CHOOSE_DIRECTORY = 3;
private static final int LOCAL_MESSAGE_LOADER_ID = 1;
private static final int DECODE_MESSAGE_LOADER_ID = 2;
public static final int REQUEST_MASK_CRYPTO_HELPER = 1 << 8;
public static final int REQUEST_MASK_CRYPTO_PRESENTER = 2 << 8;
private RetainFragment<MessageCryptoHelper> retainCryptoHelperFragment;
public static final int REQUEST_MASK_LOADER_HELPER = (1 << 8);
public static final int REQUEST_MASK_CRYPTO_PRESENTER = (1 << 9);
public static MessageViewFragment newInstance(MessageReference reference) {
MessageViewFragment fragment = new MessageViewFragment();
@ -92,12 +79,10 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
private Account mAccount;
private MessageReference mMessageReference;
private LocalMessage mMessage;
private MessageCryptoAnnotations messageAnnotations;
private MessagingController mController;
private DownloadManager downloadManager;
private Handler handler = new Handler();
private DownloadMessageListener downloadMessageListener = new DownloadMessageListener();
private MessageCryptoHelper messageCryptoHelper;
private MessageLoaderHelper messageLoaderHelper;
private MessageCryptoPresenter messageCryptoPresenter;
/**
@ -117,8 +102,6 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
private Context mContext;
private LoaderCallbacks<LocalMessage> localMessageLoaderCallback = new LocalMessageLoaderCallback();
private LoaderCallbacks<MessageViewInfo> decodeMessageLoaderCallback = new DecodeMessageLoaderCallback();
private AttachmentViewInfo currentAttachmentViewInfo;
@Override
@ -146,6 +129,8 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
mController = MessagingController.getInstance(context);
downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
messageCryptoPresenter = new MessageCryptoPresenter(this);
messageLoaderHelper =
new MessageLoaderHelper(context, getLoaderManager(), getFragmentManager(), messageLoaderCallbacks);
mInitialized = true;
}
@ -156,25 +141,11 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
Activity activity = getActivity();
boolean isChangingConfigurations = activity != null && activity.isChangingConfigurations();
if (isChangingConfigurations) {
messageCryptoHelper.detachCallback();
messageLoaderHelper.onDestroyChangingConfigurations();
return;
}
if (messageCryptoHelper != null) {
cancelAndClearMessageCryptoHelper();
}
}
@UiThread
private void cancelAndClearMessageCryptoHelper() {
if (messageCryptoHelper != null) {
messageCryptoHelper.cancelIfRunning();
messageCryptoHelper = null;
}
if (retainCryptoHelperFragment != null) {
retainCryptoHelperFragment.clearAndRemove(getFragmentManager());
retainCryptoHelperFragment = null;
}
messageLoaderHelper.onDestroy();
}
@Override
@ -199,7 +170,8 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
mMessageView.setOnDownloadButtonClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onDownloadRemainder();
mMessageView.disableDownloadButton();
messageLoaderHelper.downloadCompleteMessage();
}
});
@ -225,19 +197,17 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
}
mAccount = Preferences.getPreferences(getApplicationContext()).getAccount(mMessageReference.getAccountUuid());
startLoadingMessageFromDatabase();
messageLoaderHelper.asyncStartOrResumeLoadingMessage(messageReference);
mFragmentListener.updateMenu();
}
public void onPendingIntentResult(int requestCode, int resultCode, Intent data) {
if ((requestCode & REQUEST_MASK_CRYPTO_HELPER) == REQUEST_MASK_CRYPTO_HELPER) {
if ((requestCode & REQUEST_MASK_LOADER_HELPER) == REQUEST_MASK_LOADER_HELPER) {
hideKeyboard();
requestCode ^= REQUEST_MASK_CRYPTO_HELPER;
messageCryptoHelper.onActivityResult(requestCode, resultCode, data);
requestCode ^= REQUEST_MASK_LOADER_HELPER;
messageLoaderHelper.onActivityResult(requestCode, resultCode, data);
return;
}
@ -259,95 +229,6 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
}
}
private void startLoadingMessageFromDatabase() {
getLoaderManager().initLoader(LOCAL_MESSAGE_LOADER_ID, null, localMessageLoaderCallback);
}
@UiThread
private void onLoadMessageFromDatabaseFinished(LocalMessage message) {
displayMessageHeader(message);
mMessageView.setToLoadingState();
if (message.isBodyMissing()) {
startDownloadingMessageBody();
return;
}
if (mAccount.isOpenPgpProviderConfigured()) {
startOrResumeProcessingInCryptoHelper(message);
return;
}
startExtractingTextAndAttachments();
}
private void startOrResumeProcessingInCryptoHelper(LocalMessage message) {
retainCryptoHelperFragment =
RetainFragment.findOrCreate(getFragmentManager(), "crypto_helper_" + message.hashCode());
if (retainCryptoHelperFragment.hasData()) {
messageCryptoHelper = retainCryptoHelperFragment.getData();
} else {
messageCryptoHelper = new MessageCryptoHelper(getActivity(), mAccount.getOpenPgpProvider());
retainCryptoHelperFragment.setData(messageCryptoHelper);
}
messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, this);
}
private void onLoadMessageFromDatabaseFailed() {
// mMessageView.showStatusMessage(mContext.getString(R.string.status_invalid_id_error));
}
private void startDownloadingMessageBody() {
mController.loadMessagePartialForViewRemote(
mAccount, mMessageReference.getFolderName(), mMessageReference.getUid(), downloadMessageListener);
}
private void onMessageDownloadFinished(LocalMessage message) {
mMessage = message;
LoaderManager loaderManager = getLoaderManager();
loaderManager.destroyLoader(LOCAL_MESSAGE_LOADER_ID);
loaderManager.destroyLoader(DECODE_MESSAGE_LOADER_ID);
cancelAndClearMessageCryptoHelper();
onLoadMessageFromDatabaseFinished(mMessage);
}
private void onDownloadMessageFailed(Throwable t) {
mMessageView.enableDownloadButton();
String errorMessage;
if (t instanceof IllegalArgumentException) {
errorMessage = mContext.getString(R.string.status_invalid_id_error);
} else {
errorMessage = mContext.getString(R.string.status_network_error);
}
Toast.makeText(mContext, errorMessage, Toast.LENGTH_LONG).show();
}
@Override
public void onCryptoHelperProgress(int current, int max) {
mMessageView.setLoadingProgress(current, max);
}
@Override
public void onCryptoOperationsFinished(MessageCryptoAnnotations annotations) {
this.messageAnnotations = annotations;
startExtractingTextAndAttachments();
}
private void startExtractingTextAndAttachments() {
getLoaderManager().restartLoader(DECODE_MESSAGE_LOADER_ID, null, decodeMessageLoaderCallback);
}
private void onDecodeMessageFinished(MessageViewInfo messageViewInfo) {
if (messageViewInfo == null) {
showUnableToDecodeError();
messageViewInfo = MessageViewInfo.createWithErrorState(mMessage);
}
showMessage(messageViewInfo);
}
private void showUnableToDecodeError() {
Context context = getActivity().getApplicationContext();
Toast.makeText(context, R.string.message_view_toast_unable_to_display_message, Toast.LENGTH_SHORT).show();
@ -572,15 +453,6 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
}
}
private void onDownloadRemainder() {
if (mMessage.isSet(Flag.X_DOWNLOADED_FULL)) {
return;
}
mMessageView.disableDownloadButton();
mController.loadMessageForViewRemote(mAccount, mMessageReference.getFolderName(), mMessageReference.getUid(),
downloadMessageListener);
}
private void setProgress(boolean enable) {
if (mFragmentListener != null) {
mFragmentListener.setProgress(enable);
@ -793,21 +665,8 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
@Override
public void restartMessageCryptoProcessing() {
cancelAndClearMessageCryptoHelper();
mMessageView.setToLoadingState();
startOrResumeProcessingInCryptoHelper(mMessage);
}
@Override
public void startPendingIntentForCryptoHelper(IntentSender si, int requestCode, Intent fillIntent,
int flagsMask, int flagValues, int extraFlags) {
requestCode |= REQUEST_MASK_CRYPTO_HELPER;
try {
getActivity().startIntentSenderForResult(
si, requestCode, fillIntent, flagsMask, flagValues, extraFlags);
} catch (SendIntentException e) {
Log.e(K9.LOG_TAG, "Irrecoverable error calling PendingIntent!", e);
}
messageLoaderHelper.restartMessageCryptoProcessing();
}
@Override
@ -831,58 +690,72 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
return mInitialized ;
}
class LocalMessageLoaderCallback implements LoaderCallbacks<LocalMessage> {
@Override
public Loader<LocalMessage> onCreateLoader(int id, Bundle args) {
setProgress(true);
return new LocalMessageLoader(mContext, mController, mAccount, mMessageReference);
}
MessageLoaderCallbacks messageLoaderCallbacks = new MessageLoaderCallbacks() {
@Override
public void onLoadFinished(Loader<LocalMessage> loader, LocalMessage message) {
setProgress(false);
public void onMessageDataLoadFinished(LocalMessage message) {
mMessage = message;
if (message == null) {
onLoadMessageFromDatabaseFailed();
} else {
onLoadMessageFromDatabaseFinished(message);
}
getLoaderManager().destroyLoader(LOCAL_MESSAGE_LOADER_ID);
displayMessageHeader(message);
mMessageView.setToLoadingState();
}
@Override
public void onLoaderReset(Loader<LocalMessage> loader) {
// Do nothing
}
}
class DecodeMessageLoaderCallback implements LoaderCallbacks<MessageViewInfo> {
@Override
public Loader<MessageViewInfo> onCreateLoader(int id, Bundle args) {
if (id != DECODE_MESSAGE_LOADER_ID) {
throw new IllegalStateException("loader id must be message decoder id");
}
setProgress(true);
return new LocalMessageExtractorLoader(mContext, mMessage, messageAnnotations);
public void onMessageDataLoadFailed() {
Toast.makeText(getActivity(), R.string.status_loading_error, Toast.LENGTH_LONG).show();
}
@Override
public void onLoadFinished(Loader<MessageViewInfo> loader, MessageViewInfo messageViewInfo) {
if (loader.getId() != DECODE_MESSAGE_LOADER_ID) {
throw new IllegalStateException("loader id must be message decoder id");
}
setProgress(false);
onDecodeMessageFinished(messageViewInfo);
public void onMessageViewInfoLoadFinished(LocalMessage localMessage, MessageViewInfo messageViewInfo) {
showMessage(messageViewInfo);
}
@Override
public void onLoaderReset(Loader<MessageViewInfo> loader) {
if (loader.getId() != DECODE_MESSAGE_LOADER_ID) {
throw new IllegalStateException("loader id must be message decoder id");
}
// Do nothing
public void onMessageViewInfoLoadFailed(LocalMessage localMessage) {
MessageViewInfo messageViewInfo = MessageViewInfo.createWithErrorState(localMessage);
showMessage(messageViewInfo);
}
}
@Override
public void setLoadingProgress(int current, int max) {
mMessageView.setLoadingProgress(current, max);
}
@Override
public void onDownloadErrorMessageNotFound() {
mMessageView.enableDownloadButton();
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getActivity(), R.string.status_invalid_id_error, Toast.LENGTH_LONG).show();
}
});
}
@Override
public void onDownloadErrorNetworkError() {
mMessageView.enableDownloadButton();
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getActivity(), R.string.status_network_error, Toast.LENGTH_LONG).show();
}
});
}
@Override
public void startIntentSenderForMessageLoaderHelper(IntentSender si, int requestCode, Intent fillIntent,
int flagsMask, int flagValues, int extraFlags) {
try {
requestCode |= REQUEST_MASK_LOADER_HELPER;
getActivity().startIntentSenderForResult(
si, requestCode, fillIntent, flagsMask, flagValues, extraFlags);
} catch (SendIntentException e) {
Log.e(K9.LOG_TAG, "Irrecoverable error calling PendingIntent!", e);
}
}
};
@Override
public void onViewAttachment(AttachmentViewInfo attachment) {
@ -920,30 +793,4 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
private AttachmentController getAttachmentController(AttachmentViewInfo attachment) {
return new AttachmentController(mController, downloadManager, this, attachment);
}
private class DownloadMessageListener extends MessagingListener {
@Override
public void loadMessageForViewFinished(Account account, String folder, String uid, final LocalMessage message) {
handler.post(new Runnable() {
@Override
public void run() {
if (isAdded()) {
onMessageDownloadFinished(message);
}
}
});
}
@Override
public void loadMessageForViewFailed(Account account, String folder, String uid, final Throwable t) {
handler.post(new Runnable() {
@Override
public void run() {
if (isAdded()) {
onDownloadMessageFailed(t);
}
}
});
}
}
}

View file

@ -185,6 +185,7 @@ Please submit bug reports, contribute new features and ask questions at
<string name="status_loading_more">Loading messages\u2026</string>
<string name="status_network_error">Connection error</string>
<string name="status_invalid_id_error">Message not found</string>
<string name="status_loading_error">Message loading error</string>
<string name="status_loading_more_failed">Retry loading more messages</string>