Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b72dba67 authored by Vincent Breitmoser's avatar Vincent Breitmoser
Browse files

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

parent 0df44a14
Loading
Loading
Loading
Loading
+3 −5
Original line number Diff line number Diff line
@@ -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() {
+425 −0
Original line number Diff line number Diff line
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();
    }

}
+54 −95
Original line number Diff line number Diff line
@@ -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);
            }
            }

            // 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,18 +3847,9 @@ public class MessagingController implements Runnable {
        return (account.getRemoteStore() instanceof Pop3Store);
    }

    public void sendAlternate(final Context context, Account account, Message message) {
        if (K9.DEBUG)
            Log.d(K9.LOG_TAG, "About to load message " + account.getDescription() + ":" + message.getFolder().getName()
                  + ":" + 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) {
    public void sendAlternate(Context context, Account account, LocalMessage message) {
        if (K9.DEBUG)
                    Log.d(K9.LOG_TAG, "Got message " + account.getDescription() + ":" + folder
            Log.d(K9.LOG_TAG, "Got message " + account.getDescription() + ":" + message.getFolder()
                  + ":" + message.getUid() + " for sendAlternate");

        try {
@@ -3932,9 +3894,6 @@ public class MessagingController implements Runnable {
            Log.e(K9.LOG_TAG, "Unable to send email through alternate program", me);
        }
    }
        });

    }

    /**
     * Checks mail for one or multiple accounts. If account is null all accounts
+2 −18
Original line number Diff line number Diff line
@@ -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) {}
+4 −0

File changed.

Preview size limit exceeded, changes collapsed.

Loading