package com.fsck.k9.ui.messageview; import java.util.Collections; import java.util.Locale; import android.app.Activity; import android.app.DialogFragment; import android.app.DownloadManager; import android.app.Fragment; import android.app.FragmentManager; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.text.TextUtils; import android.view.ContextThemeWrapper; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; import com.fsck.k9.Account; 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.activity.setup.OpenPgpAppSelectDialog; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.fragment.AttachmentDownloadDialogFragment; import com.fsck.k9.fragment.ConfirmationDialogFragment; import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener; import com.fsck.k9.helper.FileBrowserHelper; import com.fsck.k9.helper.FileBrowserHelper.FileBrowserFailOverCallback; import com.fsck.k9.mail.Flag; import com.fsck.k9.mailstore.AttachmentViewInfo; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.MessageViewInfo; import com.fsck.k9.ui.messageview.CryptoInfoDialog.OnClickShowCryptoKeyListener; import com.fsck.k9.ui.messageview.MessageCryptoPresenter.MessageCryptoMvpView; import com.fsck.k9.view.MessageCryptoDisplayStatus; import com.fsck.k9.view.MessageHeader; import timber.log.Timber; public class MessageViewFragment extends Fragment implements ConfirmationDialogFragmentListener, AttachmentViewCallback, OnClickShowCryptoKeyListener { private static final String ARG_REFERENCE = "reference"; private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1; private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2; private static final int ACTIVITY_CHOOSE_DIRECTORY = 3; 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(); Bundle args = new Bundle(); args.putString(ARG_REFERENCE, reference.toIdentityString()); fragment.setArguments(args); return fragment; } private MessageTopView mMessageView; private Account mAccount; private MessageReference mMessageReference; private LocalMessage mMessage; private MessagingController mController; private DownloadManager downloadManager; private Handler handler = new Handler(); private MessageLoaderHelper messageLoaderHelper; private MessageCryptoPresenter messageCryptoPresenter; /** * Used to temporarily store the destination folder for refile operations if a confirmation * dialog is shown. */ private String mDstFolder; private MessageViewFragmentListener mFragmentListener; /** * {@code true} after {@link #onCreate(Bundle)} has been executed. This is used by * {@code MessageList.configureMenu()} to make sure the fragment has been initialized before * it is used. */ private boolean mInitialized = false; private Context mContext; private AttachmentViewInfo currentAttachmentViewInfo; @Override public void onAttach(Activity activity) { super.onAttach(activity); mContext = activity.getApplicationContext(); try { mFragmentListener = (MessageViewFragmentListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.getClass() + " must implement MessageViewFragmentListener"); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // This fragments adds options to the action bar setHasOptionsMenu(true); Context context = getActivity().getApplicationContext(); mController = MessagingController.getInstance(context); downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); messageCryptoPresenter = new MessageCryptoPresenter(savedInstanceState, messageCryptoMvpView); messageLoaderHelper = new MessageLoaderHelper(context, getLoaderManager(), getFragmentManager(), messageLoaderCallbacks); mInitialized = true; } @Override public void onResume() { super.onResume(); messageCryptoPresenter.onResume(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); messageCryptoPresenter.onSaveInstanceState(outState); } @Override public void onDestroy() { super.onDestroy(); Activity activity = getActivity(); boolean isChangingConfigurations = activity != null && activity.isChangingConfigurations(); if (isChangingConfigurations) { messageLoaderHelper.onDestroyChangingConfigurations(); return; } messageLoaderHelper.onDestroy(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Context context = new ContextThemeWrapper(inflater.getContext(), K9.getK9ThemeResourceId(K9.getK9MessageViewTheme())); LayoutInflater layoutInflater = LayoutInflater.from(context); View view = layoutInflater.inflate(R.layout.message, container, false); mMessageView = (MessageTopView) view.findViewById(R.id.message_view); mMessageView.setAttachmentCallback(this); mMessageView.setMessageCryptoPresenter(messageCryptoPresenter); mMessageView.setOnToggleFlagClickListener(new OnClickListener() { @Override public void onClick(View v) { onToggleFlagged(); } }); mMessageView.setOnDownloadButtonClickListener(new OnClickListener() { @Override public void onClick(View v) { mMessageView.disableDownloadButton(); messageLoaderHelper.downloadCompleteMessage(); } }); mFragmentListener.messageHeaderViewAvailable(mMessageView.getMessageHeaderView()); return view; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Bundle arguments = getArguments(); String messageReferenceString = arguments.getString(ARG_REFERENCE); MessageReference messageReference = MessageReference.parse(messageReferenceString); displayMessage(messageReference); } private void displayMessage(MessageReference messageReference) { mMessageReference = messageReference; Timber.d("MessageView displaying message %s", mMessageReference); mAccount = Preferences.getPreferences(getApplicationContext()).getAccount(mMessageReference.getAccountUuid()); messageLoaderHelper.asyncStartOrResumeLoadingMessage(messageReference, null); mFragmentListener.updateMenu(); } private void hideKeyboard() { Activity activity = getActivity(); if (activity == null) { return; } InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); View decorView = activity.getWindow().getDecorView(); if (decorView != null) { imm.hideSoftInputFromWindow(decorView.getApplicationWindowToken(), 0); } } private void showUnableToDecodeError() { Context context = getActivity().getApplicationContext(); Toast.makeText(context, R.string.message_view_toast_unable_to_display_message, Toast.LENGTH_SHORT).show(); } private void showMessage(MessageViewInfo messageViewInfo) { hideKeyboard(); boolean handledByCryptoPresenter = messageCryptoPresenter.maybeHandleShowMessage( mMessageView, mAccount, messageViewInfo); if (!handledByCryptoPresenter) { mMessageView.showMessage(mAccount, messageViewInfo); if (K9.isOpenPgpProviderConfigured()) { mMessageView.getMessageHeaderView().setCryptoStatusDisabled(); } else { mMessageView.getMessageHeaderView().hideCryptoStatus(); } } } private void displayHeaderForLoadingMessage(LocalMessage message) { mMessageView.setHeaders(message, mAccount); if (K9.isOpenPgpProviderConfigured()) { mMessageView.getMessageHeaderView().setCryptoStatusLoading(); } displayMessageSubject(getSubjectForMessage(message)); mFragmentListener.updateMenu(); } /** * Called from UI thread when user select Delete */ public void onDelete() { if (K9.confirmDelete() || (K9.confirmDeleteStarred() && mMessage.isSet(Flag.FLAGGED))) { showDialog(R.id.dialog_confirm_delete); } else { delete(); } } public void onToggleAllHeadersView() { mMessageView.getMessageHeaderView().onShowAdditionalHeaders(); } public boolean allHeadersVisible() { return mMessageView.getMessageHeaderView().additionalHeadersVisible(); } private void delete() { if (mMessage != null) { // Disable the delete button after it's tapped (to try to prevent // accidental clicks) mFragmentListener.disableDeleteAction(); LocalMessage messageToDelete = mMessage; mFragmentListener.showNextMessageOrReturn(); mController.deleteMessage(mMessageReference, null); } } public void onRefile(String dstFolder) { if (!mController.isMoveCapable(mAccount)) { return; } if (!mController.isMoveCapable(mMessageReference)) { Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); toast.show(); return; } if (K9.FOLDER_NONE.equalsIgnoreCase(dstFolder)) { return; } if (mAccount.getSpamFolderName().equals(dstFolder) && K9.confirmSpam()) { mDstFolder = dstFolder; showDialog(R.id.dialog_confirm_spam); } else { refileMessage(dstFolder); } } private void refileMessage(String dstFolder) { String srcFolder = mMessageReference.getFolderName(); MessageReference messageToMove = mMessageReference; mFragmentListener.showNextMessageOrReturn(); mController.moveMessage(mAccount, srcFolder, messageToMove, dstFolder); } public void onReply() { if (mMessage != null) { mFragmentListener.onReply(mMessage.makeMessageReference(), messageCryptoPresenter.getDecryptionResultForReply()); } } public void onReplyAll() { if (mMessage != null) { mFragmentListener.onReplyAll(mMessage.makeMessageReference(), messageCryptoPresenter.getDecryptionResultForReply()); } } public void onForward() { if (mMessage != null) { mFragmentListener.onForward(mMessage.makeMessageReference(), messageCryptoPresenter.getDecryptionResultForReply()); } } public void onToggleFlagged() { if (mMessage != null) { boolean newState = !mMessage.isSet(Flag.FLAGGED); mController.setFlag(mAccount, mMessage.getFolder().getName(), Collections.singletonList(mMessage), Flag.FLAGGED, newState); mMessageView.setHeaders(mMessage, mAccount); } } public void onMove() { if ((!mController.isMoveCapable(mAccount)) || (mMessage == null)) { return; } if (!mController.isMoveCapable(mMessageReference)) { Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); toast.show(); return; } startRefileActivity(ACTIVITY_CHOOSE_FOLDER_MOVE); } public void onCopy() { if ((!mController.isCopyCapable(mAccount)) || (mMessage == null)) { return; } if (!mController.isCopyCapable(mMessageReference)) { Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message, Toast.LENGTH_LONG); toast.show(); return; } startRefileActivity(ACTIVITY_CHOOSE_FOLDER_COPY); } public void onArchive() { onRefile(mAccount.getArchiveFolderName()); } public void onSpam() { onRefile(mAccount.getSpamFolderName()); } public void onSelectText() { // FIXME // mMessageView.beginSelectingText(); } private void startRefileActivity(int activity) { Intent intent = new Intent(getActivity(), ChooseFolder.class); intent.putExtra(ChooseFolder.EXTRA_ACCOUNT, mAccount.getUuid()); intent.putExtra(ChooseFolder.EXTRA_CUR_FOLDER, mMessageReference.getFolderName()); intent.putExtra(ChooseFolder.EXTRA_SEL_FOLDER, mAccount.getLastSelectedFolderName()); intent.putExtra(ChooseFolder.EXTRA_MESSAGE, mMessageReference.toIdentityString()); startActivityForResult(intent, activity); } private void startOpenPgpChooserActivity() { Intent i = new Intent(getActivity(), OpenPgpAppSelectDialog.class); startActivity(i); } public void onPendingIntentResult(int requestCode, int resultCode, Intent data) { if ((requestCode & REQUEST_MASK_LOADER_HELPER) == REQUEST_MASK_LOADER_HELPER) { requestCode ^= REQUEST_MASK_LOADER_HELPER; messageLoaderHelper.onActivityResult(requestCode, resultCode, data); return; } if ((requestCode & REQUEST_MASK_CRYPTO_PRESENTER) == REQUEST_MASK_CRYPTO_PRESENTER) { requestCode ^= REQUEST_MASK_CRYPTO_PRESENTER; messageCryptoPresenter.onActivityResult(requestCode, resultCode, data); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != Activity.RESULT_OK) { return; } // Note: because fragments do not have a startIntentSenderForResult method, pending intent activities are // launched through the MessageList activity, and delivered back via onPendingIntentResult() switch (requestCode) { case ACTIVITY_CHOOSE_DIRECTORY: { if (data != null) { // obtain the filename Uri fileUri = data.getData(); if (fileUri != null) { String filePath = fileUri.getPath(); if (filePath != null) { getAttachmentController(currentAttachmentViewInfo).saveAttachmentTo(filePath); } } } break; } case ACTIVITY_CHOOSE_FOLDER_MOVE: case ACTIVITY_CHOOSE_FOLDER_COPY: { if (data == null) { return; } String destFolderName = data.getStringExtra(ChooseFolder.EXTRA_NEW_FOLDER); String messageReferenceString = data.getStringExtra(ChooseFolder.EXTRA_MESSAGE); MessageReference ref = MessageReference.parse(messageReferenceString); if (mMessageReference.equals(ref)) { mAccount.setLastSelectedFolderName(destFolderName); switch (requestCode) { case ACTIVITY_CHOOSE_FOLDER_MOVE: { mFragmentListener.showNextMessageOrReturn(); moveMessage(ref, destFolderName); break; } case ACTIVITY_CHOOSE_FOLDER_COPY: { copyMessage(ref, destFolderName); break; } } } break; } } } public void onSendAlternate() { if (mMessage != null) { mController.sendAlternate(getActivity(), mAccount, mMessage); } } public void onToggleRead() { if (mMessage != null) { mController.setFlag(mAccount, mMessage.getFolder().getName(), Collections.singletonList(mMessage), Flag.SEEN, !mMessage.isSet(Flag.SEEN)); mMessageView.setHeaders(mMessage, mAccount); String subject = mMessage.getSubject(); displayMessageSubject(subject); mFragmentListener.updateMenu(); } } private void setProgress(boolean enable) { if (mFragmentListener != null) { mFragmentListener.setProgress(enable); } } private void displayMessageSubject(String subject) { if (mFragmentListener != null) { mFragmentListener.displayMessageSubject(subject); } } private String getSubjectForMessage(LocalMessage message) { String subject = message.getSubject(); if (TextUtils.isEmpty(subject)) { return mContext.getString(R.string.general_no_subject); } return subject; } public void moveMessage(MessageReference reference, String destFolderName) { mController.moveMessage(mAccount, mMessageReference.getFolderName(), reference, destFolderName); } public void copyMessage(MessageReference reference, String destFolderName) { mController.copyMessage(mAccount, mMessageReference.getFolderName(), reference, destFolderName); } private void showDialog(int dialogId) { DialogFragment fragment; switch (dialogId) { case R.id.dialog_confirm_delete: { String title = getString(R.string.dialog_confirm_delete_title); String message = getString(R.string.dialog_confirm_delete_message); String confirmText = getString(R.string.dialog_confirm_delete_confirm_button); String cancelText = getString(R.string.dialog_confirm_delete_cancel_button); fragment = ConfirmationDialogFragment.newInstance(dialogId, title, message, confirmText, cancelText); break; } case R.id.dialog_confirm_spam: { String title = getString(R.string.dialog_confirm_spam_title); String message = getResources().getQuantityString(R.plurals.dialog_confirm_spam_message, 1); String confirmText = getString(R.string.dialog_confirm_spam_confirm_button); String cancelText = getString(R.string.dialog_confirm_spam_cancel_button); fragment = ConfirmationDialogFragment.newInstance(dialogId, title, message, confirmText, cancelText); break; } case R.id.dialog_attachment_progress: { String message = getString(R.string.dialog_attachment_progress_title); int size = (int) currentAttachmentViewInfo.size; fragment = AttachmentDownloadDialogFragment.newInstance(size, message); break; } default: { throw new RuntimeException("Called showDialog(int) with unknown dialog id."); } } fragment.setTargetFragment(this, dialogId); fragment.show(getFragmentManager(), getDialogTag(dialogId)); } private void removeDialog(int dialogId) { FragmentManager fm = getFragmentManager(); if (fm == null || isRemoving() || isDetached()) { return; } // Make sure the "show dialog" transaction has been processed when we call // findFragmentByTag() below. Otherwise the fragment won't be found and the dialog will // never be dismissed. fm.executePendingTransactions(); DialogFragment fragment = (DialogFragment) fm.findFragmentByTag(getDialogTag(dialogId)); if (fragment != null) { fragment.dismiss(); } } private String getDialogTag(int dialogId) { return String.format(Locale.US, "dialog-%d", dialogId); } public void zoom(KeyEvent event) { // mMessageView.zoom(event); } @Override public void doPositiveClick(int dialogId) { switch (dialogId) { case R.id.dialog_confirm_delete: { delete(); break; } case R.id.dialog_confirm_spam: { refileMessage(mDstFolder); mDstFolder = null; break; } } } @Override public void doNegativeClick(int dialogId) { /* do nothing */ } @Override public void dialogCancelled(int dialogId) { /* do nothing */ } /** * Get the {@link MessageReference} of the currently displayed message. */ public MessageReference getMessageReference() { return mMessageReference; } public boolean isMessageRead() { return (mMessage != null) ? mMessage.isSet(Flag.SEEN) : false; } public boolean isCopyCapable() { return mController.isCopyCapable(mAccount); } public boolean isMoveCapable() { return mController.isMoveCapable(mAccount); } public boolean canMessageBeArchived() { return (!mMessageReference.getFolderName().equals(mAccount.getArchiveFolderName()) && mAccount.hasArchiveFolder()); } public boolean canMessageBeMovedToSpam() { return (!mMessageReference.getFolderName().equals(mAccount.getSpamFolderName()) && mAccount.hasSpamFolder()); } public void updateTitle() { if (mMessage != null) { displayMessageSubject(mMessage.getSubject()); } } public Context getApplicationContext() { return mContext; } public void disableAttachmentButtons(AttachmentViewInfo attachment) { // mMessageView.disableAttachmentButtons(attachment); } public void enableAttachmentButtons(AttachmentViewInfo attachment) { // mMessageView.enableAttachmentButtons(attachment); } public void runOnMainThread(Runnable runnable) { handler.post(runnable); } public void showAttachmentLoadingDialog() { // mMessageView.disableAttachmentButtons(); showDialog(R.id.dialog_attachment_progress); } public void hideAttachmentLoadingDialogOnMainThread() { handler.post(new Runnable() { @Override public void run() { removeDialog(R.id.dialog_attachment_progress); // mMessageView.enableAttachmentButtons(); } }); } public void refreshAttachmentThumbnail(AttachmentViewInfo attachment) { // mMessageView.refreshAttachmentThumbnail(attachment); } private MessageCryptoMvpView messageCryptoMvpView = new MessageCryptoMvpView() { @Override public void redisplayMessage() { messageLoaderHelper.asyncReloadMessage(); } @Override public void startPendingIntentForCryptoPresenter(IntentSender si, Integer requestCode, Intent fillIntent, int flagsMask, int flagValues, int extraFlags) throws SendIntentException { if (requestCode == null) { getActivity().startIntentSender(si, fillIntent, flagsMask, flagValues, extraFlags); return; } requestCode |= REQUEST_MASK_CRYPTO_PRESENTER; getActivity().startIntentSenderForResult( si, requestCode, fillIntent, flagsMask, flagValues, extraFlags); } @Override public void showCryptoInfoDialog(MessageCryptoDisplayStatus displayStatus, boolean hasSecurityWarning) { CryptoInfoDialog dialog = CryptoInfoDialog.newInstance(displayStatus, hasSecurityWarning); dialog.setTargetFragment(MessageViewFragment.this, 0); dialog.show(getFragmentManager(), "crypto_info_dialog"); } @Override public void restartMessageCryptoProcessing() { mMessageView.setToLoadingState(); messageLoaderHelper.asyncRestartMessageCryptoProcessing(); } @Override public void showCryptoConfigDialog() { startOpenPgpChooserActivity(); } }; @Override public void onClickShowSecurityWarning() { messageCryptoPresenter.onClickShowCryptoWarningDetails(); } @Override public void onClickShowCryptoKey() { messageCryptoPresenter.onClickShowCryptoKey(); } public interface MessageViewFragmentListener { void onForward(MessageReference messageReference, Parcelable decryptionResultForReply); void disableDeleteAction(); void onReplyAll(MessageReference messageReference, Parcelable decryptionResultForReply); void onReply(MessageReference messageReference, Parcelable decryptionResultForReply); void displayMessageSubject(String title); void setProgress(boolean b); void showNextMessageOrReturn(); void messageHeaderViewAvailable(MessageHeader messageHeaderView); void updateMenu(); } public boolean isInitialized() { return mInitialized ; } private MessageLoaderCallbacks messageLoaderCallbacks = new MessageLoaderCallbacks() { @Override public void onMessageDataLoadFinished(LocalMessage message) { mMessage = message; displayHeaderForLoadingMessage(message); mMessageView.setToLoadingState(); } @Override public void onMessageDataLoadFailed() { Toast.makeText(getActivity(), R.string.status_loading_error, Toast.LENGTH_LONG).show(); } @Override public void onMessageViewInfoLoadFinished(MessageViewInfo messageViewInfo) { showMessage(messageViewInfo); } @Override public void onMessageViewInfoLoadFailed(MessageViewInfo messageViewInfo) { 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) { Timber.e(e, "Irrecoverable error calling PendingIntent!"); } } }; @Override public void onViewAttachment(AttachmentViewInfo attachment) { currentAttachmentViewInfo = attachment; getAttachmentController(attachment).viewAttachment(); } @Override public void onSaveAttachment(AttachmentViewInfo attachment) { currentAttachmentViewInfo = attachment; getAttachmentController(attachment).saveAttachment(); } @Override public void onSaveAttachmentToUserProvidedDirectory(final AttachmentViewInfo attachment) { currentAttachmentViewInfo = attachment; FileBrowserHelper.getInstance().showFileBrowserActivity(MessageViewFragment.this, null, ACTIVITY_CHOOSE_DIRECTORY, new FileBrowserFailOverCallback() { @Override public void onPathEntered(String path) { getAttachmentController(attachment).saveAttachmentTo(path); } @Override public void onCancel() { // Do nothing } }); } private AttachmentController getAttachmentController(AttachmentViewInfo attachment) { return new AttachmentController(mController, downloadManager, this, attachment); } }