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.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import timber.log.Timber;
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.controller.SimpleMessagingListener;
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;
import org.openintents.openpgp.OpenPgpDecryptionResult;
/** 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 - all of this may be cleared to avoid data leakage!
private Context context;
private FragmentManager fragmentManager;
private LoaderManager loaderManager;
@Nullable // make this explicitly nullable, make sure to cancel/ignore any operation if this is null
private MessageLoaderCallbacks callback;
// transient state
private MessageReference messageReference;
private Account account;
private LocalMessage localMessage;
private MessageCryptoAnnotations messageCryptoAnnotations;
private OpenPgpDecryptionResult cachedDecryptionResult;
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, Parcelable cachedDecryptionResult) {
this.messageReference = messageReference;
this.account = Preferences.getPreferences(context).getAccount(messageReference.getAccountUuid());
if (cachedDecryptionResult != null) {
if (cachedDecryptionResult instanceof OpenPgpDecryptionResult) {
this.cachedDecryptionResult = (OpenPgpDecryptionResult) cachedDecryptionResult;
} else {
Timber.e("Got decryption result of unknown type - ignoring");
}
}
startOrResumeLocalMessageLoader();
}
@UiThread
public void asyncReloadMessage() {
startOrResumeLocalMessageLoader();
}
@UiThread
public void asyncRestartMessageCryptoProcessing() {
cancelAndClearCryptoOperation();
cancelAndClearDecodeLoader();
if (K9.isOpenPgpProviderConfigured()) {
startOrResumeCryptoOperation();
} else {
startOrResumeDecodeMessage();
}
}
/** Cancels all loading processes, prevents future callbacks, and destroys all loading state. */
@UiThread
public void onDestroy() {
if (messageCryptoHelper != null) {
messageCryptoHelper.cancelIfRunning();
}
callback = null;
context = null;
fragmentManager = null;
loaderManager = 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() {
cancelAndClearDecodeLoader();
if (messageCryptoHelper != null) {
messageCryptoHelper.detachCallback();
}
callback = null;
context = null;
fragmentManager = null;
loaderManager = null;
}
@UiThread
public void downloadCompleteMessage() {
startDownloadingMessageBody(true);
}
@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) {
Timber.d("Creating new local message loader");
cancelAndClearCryptoOperation();
cancelAndClearDecodeLoader();
loaderManager.restartLoader(LOCAL_MESSAGE_LOADER_ID, null, localMessageLoaderCallback);
} else {
Timber.d("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);
boolean messageIncomplete =
!localMessage.isSet(Flag.X_DOWNLOADED_FULL) && !localMessage.isSet(Flag.X_DOWNLOADED_PARTIAL);
if (messageIncomplete) {
startDownloadingMessageBody(false);
return;
}
if (K9.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(true);
if (retainCryptoHelperFragment.hasData()) {
messageCryptoHelper = retainCryptoHelperFragment.getData();
}
if (messageCryptoHelper == null || messageCryptoHelper.isConfiguredForOutdatedCryptoProvider()) {
messageCryptoHelper = new MessageCryptoHelper(context);
retainCryptoHelperFragment.setData(messageCryptoHelper);
}
messageCryptoHelper.asyncStartOrResumeProcessingMessage(
localMessage, messageCryptoCallback, cachedDecryptionResult);
}
private void cancelAndClearCryptoOperation() {
RetainFragment<MessageCryptoHelper> retainCryptoHelperFragment = getMessageCryptoHelperRetainFragment(false);
if (retainCryptoHelperFragment != null) {
if (retainCryptoHelperFragment.hasData()) {
messageCryptoHelper = retainCryptoHelperFragment.getData();
messageCryptoHelper.cancelIfRunning();
messageCryptoHelper = null;
}
retainCryptoHelperFragment.clearAndRemove(fragmentManager);
}
}
private RetainFragment<MessageCryptoHelper> getMessageCryptoHelperRetainFragment(boolean createIfNotExists) {
if (createIfNotExists) {
return RetainFragment.findOrCreate(fragmentManager, "crypto_helper_" + messageReference.hashCode());
} else {
return RetainFragment.findOrNull(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) {
Timber.d("Creating new decode message loader");
loaderManager.restartLoader(DECODE_MESSAGE_LOADER_ID, null, decodeMessageLoaderCallback);
} else {
Timber.d("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) {
messageViewInfo = createErrorStateMessageViewInfo();
callback.onMessageViewInfoLoadFailed(messageViewInfo);
return;
}
callback.onMessageViewInfoLoadFinished(messageViewInfo);
}
@NonNull
private MessageViewInfo createErrorStateMessageViewInfo() {
boolean isMessageIncomplete = !localMessage.isSet(Flag.X_DOWNLOADED_FULL);
return MessageViewInfo.createWithErrorState(localMessage, isMessageIncomplete);
}
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 SimpleMessagingListener() {
@Override
public void loadMessageRemoteFinished(Account account, String folder, String uid) {
if (!messageReference.equals(account.getUuid(), folder, uid)) {
return;
}
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(MessageViewInfo messageViewInfo);
void onMessageViewInfoLoadFailed(MessageViewInfo messageViewInfo);
void setLoadingProgress(int current, int max);
void startIntentSenderForMessageLoaderHelper(IntentSender si, int requestCode, Intent fillIntent, int flagsMask,
int flagValues, int extraFlags);
void onDownloadErrorMessageNotFound();
void onDownloadErrorNetworkError();
}
}