package com.fsck.k9.activity.compose; import java.util.ArrayList; import java.util.LinkedHashMap; import android.annotation.TargetApi; import android.app.Activity; import android.app.LoaderManager; import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.content.Loader; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import com.fsck.k9.activity.compose.ComposeCryptoStatus.AttachErrorState; import com.fsck.k9.activity.loader.AttachmentContentLoader; import com.fsck.k9.activity.loader.AttachmentInfoLoader; import com.fsck.k9.activity.misc.Attachment; import com.fsck.k9.activity.misc.Attachment.LoadingState; import com.fsck.k9.mailstore.AttachmentViewInfo; import com.fsck.k9.mailstore.MessageViewInfo; public class AttachmentPresenter { private static final String STATE_KEY_ATTACHMENTS = "com.fsck.k9.activity.MessageCompose.attachments"; private static final String STATE_KEY_WAITING_FOR_ATTACHMENTS = "waitingForAttachments"; private static final String STATE_KEY_NEXT_LOADER_ID = "nextLoaderId"; private static final String LOADER_ARG_ATTACHMENT = "attachment"; private static final int LOADER_ID_MASK = 1 << 6; private static final int MAX_TOTAL_LOADERS = LOADER_ID_MASK - 1; private static final int REQUEST_CODE_ATTACHMENT_URI = 1; // injected state private final Context context; private final AttachmentMvpView attachmentMvpView; private final LoaderManager loaderManager; private final AttachmentsChangedListener listener; // persistent state private LinkedHashMap<Uri, Attachment> attachments; private int nextLoaderId = 0; private WaitingAction actionToPerformAfterWaiting = WaitingAction.NONE; public AttachmentPresenter(Context context, AttachmentMvpView attachmentMvpView, LoaderManager loaderManager, AttachmentsChangedListener listener) { this.context = context; this.attachmentMvpView = attachmentMvpView; this.loaderManager = loaderManager; this.listener = listener; attachments = new LinkedHashMap<>(); } public void onSaveInstanceState(Bundle outState) { outState.putString(STATE_KEY_WAITING_FOR_ATTACHMENTS, actionToPerformAfterWaiting.name()); outState.putParcelableArrayList(STATE_KEY_ATTACHMENTS, createAttachmentList()); outState.putInt(STATE_KEY_NEXT_LOADER_ID, nextLoaderId); } public void onRestoreInstanceState(Bundle savedInstanceState) { actionToPerformAfterWaiting = WaitingAction.valueOf( savedInstanceState.getString(STATE_KEY_WAITING_FOR_ATTACHMENTS)); nextLoaderId = savedInstanceState.getInt(STATE_KEY_NEXT_LOADER_ID); ArrayList<Attachment> attachmentList = savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS); // noinspection ConstantConditions, we know this is set in onSaveInstanceState for (Attachment attachment : attachmentList) { attachments.put(attachment.uri, attachment); attachmentMvpView.addAttachmentView(attachment); if (attachment.state == LoadingState.URI_ONLY) { initAttachmentInfoLoader(attachment); } else if (attachment.state == LoadingState.METADATA) { initAttachmentContentLoader(attachment); } } } public boolean checkOkForSendingOrDraftSaving() { if (actionToPerformAfterWaiting != WaitingAction.NONE) { return true; } if (hasLoadingAttachments()) { actionToPerformAfterWaiting = WaitingAction.SEND; attachmentMvpView.showWaitingForAttachmentDialog(actionToPerformAfterWaiting); return true; } return false; } private boolean hasLoadingAttachments() { for (Attachment attachment : attachments.values()) { Loader loader = loaderManager.getLoader(attachment.loaderId); if (loader != null && loader.isStarted()) { return true; } } return false; } public ArrayList<Attachment> createAttachmentList() { ArrayList<Attachment> result = new ArrayList<>(); for (Attachment attachment : attachments.values()) { result.add(attachment); } return result; } public void onClickAddAttachment(RecipientPresenter recipientPresenter) { AttachErrorState maybeAttachErrorState = recipientPresenter.getCurrentCryptoStatus().getAttachErrorStateOrNull(); if (maybeAttachErrorState != null) { recipientPresenter.showPgpAttachError(maybeAttachErrorState); return; } attachmentMvpView.showPickAttachmentDialog(REQUEST_CODE_ATTACHMENT_URI); } private void addAttachment(Uri uri) { addAttachment(uri, null); } private void addAttachment(AttachmentViewInfo attachmentViewInfo) { if (attachments.containsKey(attachmentViewInfo.internalUri)) { throw new IllegalStateException("Received the same attachmentViewInfo twice!"); } int loaderId = getNextFreeLoaderId(); Attachment attachment = Attachment.createAttachment( attachmentViewInfo.internalUri, loaderId, attachmentViewInfo.mimeType); attachment = attachment.deriveWithMetadataLoaded( attachmentViewInfo.mimeType, attachmentViewInfo.displayName, attachmentViewInfo.size); addAttachmentAndStartLoader(attachment); } public void addAttachment(Uri uri, String contentType) { if (attachments.containsKey(uri)) { return; } int loaderId = getNextFreeLoaderId(); Attachment attachment = Attachment.createAttachment(uri, loaderId, contentType); addAttachmentAndStartLoader(attachment); } public boolean loadNonInlineAttachments(MessageViewInfo messageViewInfo) { boolean allPartsAvailable = true; for (AttachmentViewInfo attachmentViewInfo : messageViewInfo.attachments) { if (attachmentViewInfo.inlineAttachment) { continue; } if (!attachmentViewInfo.isContentAvailable()) { allPartsAvailable = false; continue; } addAttachment(attachmentViewInfo); } return allPartsAvailable; } public void processMessageToForward(MessageViewInfo messageViewInfo) { boolean isMissingParts = !loadNonInlineAttachments(messageViewInfo); if (isMissingParts) { attachmentMvpView.showMissingAttachmentsPartialMessageWarning(); } } private void addAttachmentAndStartLoader(Attachment attachment) { attachments.put(attachment.uri, attachment); listener.onAttachmentAdded(); attachmentMvpView.addAttachmentView(attachment); if (attachment.state == LoadingState.URI_ONLY) { initAttachmentInfoLoader(attachment); } else if (attachment.state == LoadingState.METADATA) { initAttachmentContentLoader(attachment); } else { throw new IllegalStateException("Attachment can only be added in URI_ONLY or METADATA state!"); } } private void initAttachmentInfoLoader(Attachment attachment) { if (attachment.state != LoadingState.URI_ONLY) { throw new IllegalStateException("initAttachmentInfoLoader can only be called for URI_ONLY state!"); } Bundle bundle = new Bundle(); bundle.putParcelable(LOADER_ARG_ATTACHMENT, attachment.uri); loaderManager.initLoader(attachment.loaderId, bundle, mAttachmentInfoLoaderCallback); } private void initAttachmentContentLoader(Attachment attachment) { if (attachment.state != LoadingState.METADATA) { throw new IllegalStateException("initAttachmentContentLoader can only be called for METADATA state!"); } Bundle bundle = new Bundle(); bundle.putParcelable(LOADER_ARG_ATTACHMENT, attachment.uri); loaderManager.initLoader(attachment.loaderId, bundle, mAttachmentContentLoaderCallback); } private int getNextFreeLoaderId() { if (nextLoaderId >= MAX_TOTAL_LOADERS) { throw new AssertionError("more than " + MAX_TOTAL_LOADERS + " attachments? hum."); } return LOADER_ID_MASK | nextLoaderId++; } private LoaderManager.LoaderCallbacks<Attachment> mAttachmentInfoLoaderCallback = new LoaderManager.LoaderCallbacks<Attachment>() { @Override public Loader<Attachment> onCreateLoader(int id, Bundle args) { Uri uri = args.getParcelable(LOADER_ARG_ATTACHMENT); return new AttachmentInfoLoader(context, attachments.get(uri)); } @Override public void onLoadFinished(Loader<Attachment> loader, Attachment attachment) { int loaderId = loader.getId(); loaderManager.destroyLoader(loaderId); if (!attachments.containsKey(attachment.uri)) { return; } attachmentMvpView.updateAttachmentView(attachment); attachments.put(attachment.uri, attachment); initAttachmentContentLoader(attachment); } @Override public void onLoaderReset(Loader<Attachment> loader) { // nothing to do } }; private LoaderManager.LoaderCallbacks<Attachment> mAttachmentContentLoaderCallback = new LoaderManager.LoaderCallbacks<Attachment>() { @Override public Loader<Attachment> onCreateLoader(int id, Bundle args) { Uri uri = args.getParcelable(LOADER_ARG_ATTACHMENT); return new AttachmentContentLoader(context, attachments.get(uri)); } @Override public void onLoadFinished(Loader<Attachment> loader, Attachment attachment) { int loaderId = loader.getId(); loaderManager.destroyLoader(loaderId); if (!attachments.containsKey(attachment.uri)) { return; } if (attachment.state == Attachment.LoadingState.COMPLETE) { attachmentMvpView.updateAttachmentView(attachment); attachments.put(attachment.uri, attachment); } else { attachments.remove(attachment.uri); attachmentMvpView.removeAttachmentView(attachment); } postPerformStalledAction(); } @Override public void onLoaderReset(Loader<Attachment> loader) { // nothing to do } }; private void postPerformStalledAction() { new Handler().post(new Runnable() { @Override public void run() { performStalledAction(); } }); } private void performStalledAction() { attachmentMvpView.dismissWaitingForAttachmentDialog(); WaitingAction waitingFor = actionToPerformAfterWaiting; actionToPerformAfterWaiting = WaitingAction.NONE; switch (waitingFor) { case SEND: { attachmentMvpView.performSendAfterChecks(); break; } case SAVE: { attachmentMvpView.performSaveAfterChecks(); break; } } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void addAttachmentsFromResultIntent(Intent data) { // TODO draftNeedsSaving = true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { ClipData clipData = data.getClipData(); if (clipData != null) { for (int i = 0, end = clipData.getItemCount(); i < end; i++) { Uri uri = clipData.getItemAt(i).getUri(); if (uri != null) { addAttachment(uri); } } return; } } Uri uri = data.getData(); if (uri != null) { addAttachment(uri); } } public void attachmentProgressDialogCancelled() { actionToPerformAfterWaiting = WaitingAction.NONE; } public void onClickRemoveAttachment(Uri uri) { Attachment attachment = attachments.get(uri); loaderManager.destroyLoader(attachment.loaderId); attachmentMvpView.removeAttachmentView(attachment); attachments.remove(uri); listener.onAttachmentRemoved(); } public void onActivityResult(int resultCode, int requestCode, Intent data) { if (requestCode != REQUEST_CODE_ATTACHMENT_URI) { throw new AssertionError("onActivityResult must only be called for our request code"); } if (resultCode != Activity.RESULT_OK) { return; } if (data == null) { return; } addAttachmentsFromResultIntent(data); } public enum WaitingAction { NONE, SEND, SAVE } public interface AttachmentMvpView { void showWaitingForAttachmentDialog(WaitingAction waitingAction); void dismissWaitingForAttachmentDialog(); void showPickAttachmentDialog(int requestCode); void addAttachmentView(Attachment attachment); void removeAttachmentView(Attachment attachment); void updateAttachmentView(Attachment attachment); // TODO these should not really be here :\ void performSendAfterChecks(); void performSaveAfterChecks(); void showMissingAttachmentsPartialMessageWarning(); } public interface AttachmentsChangedListener { void onAttachmentAdded(); void onAttachmentRemoved(); } }