diff --git a/res/layout/message_compose_attachment.xml b/res/layout/message_compose_attachment.xml index 263d5a13c..60546e184 100644 --- a/res/layout/message_compose_attachment.xml +++ b/res/layout/message_compose_attachment.xml @@ -1,36 +1,55 @@ - - - + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="54dip" + android:paddingRight="6dip" + android:paddingTop="6dip" + android:paddingBottom="6dip"> + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 5e4682632..af4b073a9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1152,4 +1152,9 @@ Please submit bug reports, contribute new features and ask questions at Visible message actions Show selected actions in the message view menu + + Loading attachment… + Sending message + Saving draft + Fetching attachment… diff --git a/src/com/fsck/k9/activity/K9Activity.java b/src/com/fsck/k9/activity/K9Activity.java index 64083758d..82ffc3835 100644 --- a/src/com/fsck/k9/activity/K9Activity.java +++ b/src/com/fsck/k9/activity/K9Activity.java @@ -3,12 +3,12 @@ package com.fsck.k9.activity; import android.os.Bundle; import android.view.MotionEvent; -import com.actionbarsherlock.app.SherlockActivity; +import com.actionbarsherlock.app.SherlockFragmentActivity; import com.fsck.k9.activity.K9ActivityCommon.K9ActivityMagic; import com.fsck.k9.activity.misc.SwipeGestureDetector.OnSwipeGestureListener; -public class K9Activity extends SherlockActivity implements K9ActivityMagic { +public class K9Activity extends SherlockFragmentActivity implements K9ActivityMagic { private K9ActivityCommon mBase; diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index 9823e4a37..8d6b5fb7d 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -5,19 +5,18 @@ import android.annotation.TargetApi; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; -import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; -import android.provider.OpenableColumns; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; import android.text.TextWatcher; import android.text.util.Rfc822Tokenizer; import android.util.Log; @@ -55,10 +54,14 @@ import com.fsck.k9.Identity; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; +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.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.crypto.CryptoProvider; import com.fsck.k9.crypto.PgpData; +import com.fsck.k9.fragment.ProgressDialogFragment; import com.fsck.k9.helper.ContactItem; import com.fsck.k9.helper.Contacts; import com.fsck.k9.helper.HtmlConverter; @@ -79,7 +82,8 @@ import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody; -import com.fsck.k9.mail.store.LocalStore.LocalAttachmentMessageBody; +import com.fsck.k9.mail.store.LocalStore.TempFileBody; +import com.fsck.k9.mail.store.LocalStore.TempFileMessageBody; import com.fsck.k9.view.MessageWebView; import org.apache.james.mime4j.codec.EncoderUtil; import org.apache.james.mime4j.util.MimeUtil; @@ -87,8 +91,6 @@ import org.htmlcleaner.CleanerProperties; import org.htmlcleaner.HtmlCleaner; import org.htmlcleaner.SimpleHtmlSerializer; import org.htmlcleaner.TagNode; -import java.io.File; -import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -103,7 +105,9 @@ import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class MessageCompose extends K9Activity implements OnClickListener { +public class MessageCompose extends K9Activity implements OnClickListener, + ProgressDialogFragment.CancelListener { + private static final int DIALOG_SAVE_OR_DISCARD_DRAFT_MESSAGE = 1; private static final int DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED = 2; private static final int DIALOG_CONTINUE_WITHOUT_PUBLIC_KEY = 3; @@ -147,12 +151,19 @@ public class MessageCompose extends K9Activity implements OnClickListener { "com.fsck.k9.activity.MessageCompose.forcePlainText"; private static final String STATE_KEY_QUOTED_TEXT_FORMAT = "com.fsck.k9.activity.MessageCompose.quotedTextFormat"; + private static final String STATE_KEY_NUM_ATTACHMENTS_LOADING = "numAttachmentsLoading"; + private static final String STATE_KEY_WAITING_FOR_ATTACHMENTS = "waitingForAttachments"; + + private static final String LOADER_ARG_ATTACHMENT = "attachment"; + + private static final String FRAGMENT_WAITING_FOR_ATTACHMENT = "waitingForAttachment"; private static final int MSG_PROGRESS_ON = 1; private static final int MSG_PROGRESS_OFF = 2; private static final int MSG_SKIPPED_ATTACHMENTS = 3; private static final int MSG_SAVED_DRAFT = 4; private static final int MSG_DISCARDED_DRAFT = 5; + private static final int MSG_PERFORM_STALLED_ACTION = 6; private static final int ACTIVITY_REQUEST_PICK_ATTACHMENT = 1; private static final int CONTACT_PICKER_TO = 4; @@ -219,6 +230,7 @@ public class MessageCompose extends K9Activity implements OnClickListener { * have already been added from the restore of the view state. */ private boolean mSourceMessageProcessed = false; + private int mMaxLoaderId = 0; enum Action { COMPOSE, @@ -323,6 +335,23 @@ public class MessageCompose extends K9Activity implements OnClickListener { */ private long mDraftId = INVALID_DRAFT_ID; + /** + * Number of attachments currently being fetched. + */ + private int mNumAttachmentsLoading = 0; + + private enum WaitingAction { + NONE, + SEND, + SAVE + } + + /** + * Specifies what action to perform once attachments have been fetched. + */ + private WaitingAction mWaitingForAttachments = WaitingAction.NONE; + + private Handler mHandler = new Handler() { @Override public void handleMessage(android.os.Message msg) { @@ -351,6 +380,9 @@ public class MessageCompose extends K9Activity implements OnClickListener { getString(R.string.message_discarded_toast), Toast.LENGTH_LONG).show(); break; + case MSG_PERFORM_STALLED_ACTION: + performStalledAction(); + break; default: super.handleMessage(msg); break; @@ -366,14 +398,6 @@ public class MessageCompose extends K9Activity implements OnClickListener { private ContextThemeWrapper mThemeContext; - static class Attachment implements Serializable { - private static final long serialVersionUID = 3642382876618963734L; - public String name; - public String contentType; - public long size; - public Uri uri; - } - /** * Compose a new message using the given account. If account is null the default account * will be used. @@ -1078,12 +1102,15 @@ public class MessageCompose extends K9Activity implements OnClickListener { @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - ArrayList attachments = new ArrayList(); + ArrayList attachments = new ArrayList(); for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { View view = mAttachments.getChildAt(i); Attachment attachment = (Attachment) view.getTag(); - attachments.add(attachment.uri); + attachments.add(attachment); } + + outState.putInt(STATE_KEY_NUM_ATTACHMENTS_LOADING, mNumAttachmentsLoading); + outState.putString(STATE_KEY_WAITING_FOR_ATTACHMENTS, mWaitingForAttachments.name()); outState.putParcelableArrayList(STATE_KEY_ATTACHMENTS, attachments); outState.putBoolean(STATE_KEY_CC_SHOWN, mCcWrapper.getVisibility() == View.VISIBLE); outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccWrapper.getVisibility() == View.VISIBLE); @@ -1105,11 +1132,32 @@ public class MessageCompose extends K9Activity implements OnClickListener { @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); - ArrayList attachments = savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS); + mAttachments.removeAllViews(); - for (Parcelable p : attachments) { - Uri uri = (Uri) p; - addAttachment(uri); + mMaxLoaderId = 0; + + mNumAttachmentsLoading = savedInstanceState.getInt(STATE_KEY_NUM_ATTACHMENTS_LOADING); + mWaitingForAttachments = WaitingAction.NONE; + try { + String waitingFor = savedInstanceState.getString(STATE_KEY_WAITING_FOR_ATTACHMENTS); + mWaitingForAttachments = WaitingAction.valueOf(waitingFor); + } catch (Exception e) { + Log.w(K9.LOG_TAG, "Couldn't read value \" + STATE_KEY_WAITING_FOR_ATTACHMENTS +" + + "\" from saved instance state", e); + } + + ArrayList attachments = savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS); + for (Attachment attachment : attachments) { + addAttachmentView(attachment); + if (attachment.loaderId > mMaxLoaderId) { + mMaxLoaderId = attachment.loaderId; + } + + if (attachment.state == Attachment.LoadingState.URI_ONLY) { + initAttachmentInfoLoader(attachment); + } else if (attachment.state == Attachment.LoadingState.METADATA) { + initAttachmentContentLoader(attachment); + } } mReadReceipt = savedInstanceState @@ -1472,15 +1520,19 @@ public class MessageCompose extends K9Activity implements OnClickListener { * @throws MessagingException */ private void addAttachmentsToMessage(final MimeMultipart mp) throws MessagingException { - LocalAttachmentBody body; + Body body; for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag(); + + if (attachment.state != Attachment.LoadingState.COMPLETE) { + continue; + } + String contentType = attachment.contentType; if (MimeUtil.isMessage(contentType)) { - body = new LocalAttachmentMessageBody(attachment.uri, - getApplication()); + body = new TempFileMessageBody(attachment.filename); } else { - body = new LocalAttachmentBody(attachment.uri, getApplication()); + body = new TempFileBody(attachment.filename); } MimeBodyPart bp = new MimeBodyPart(body); @@ -1780,6 +1832,20 @@ public class MessageCompose extends K9Activity implements OnClickListener { Toast.makeText(this, getString(R.string.message_compose_error_no_recipients), Toast.LENGTH_LONG).show(); return; } + + if (mWaitingForAttachments != WaitingAction.NONE) { + return; + } + + if (mNumAttachmentsLoading > 0) { + mWaitingForAttachments = WaitingAction.SEND; + showWaitingForAttachmentDialog(); + } else { + performSend(); + } + } + + private void performSend() { final CryptoProvider crypto = mAccount.getCryptoProvider(); if (mEncryptCheckbox.isChecked() && !mPgpData.hasEncryptionKeys()) { // key selection before encryption @@ -1843,6 +1909,19 @@ public class MessageCompose extends K9Activity implements OnClickListener { } private void onSave() { + if (mWaitingForAttachments != WaitingAction.NONE) { + return; + } + + if (mNumAttachmentsLoading > 0) { + mWaitingForAttachments = WaitingAction.SAVE; + showWaitingForAttachmentDialog(); + } else { + performSend(); + } + } + + private void performSave() { saveIfNeeded(); finish(); } @@ -1918,71 +1997,169 @@ public class MessageCompose extends K9Activity implements OnClickListener { } private void addAttachment(Uri uri, String contentType) { - long size = -1; - String name = null; - - ContentResolver contentResolver = getContentResolver(); - - Cursor metadataCursor = contentResolver.query( - uri, - new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }, - null, - null, - null); - - if (metadataCursor != null) { - try { - if (metadataCursor.moveToFirst()) { - name = metadataCursor.getString(0); - size = metadataCursor.getInt(1); - } - } finally { - metadataCursor.close(); - } - } - - if (name == null) { - name = uri.getLastPathSegment(); - } - - String usableContentType = contentType; - if ((usableContentType == null) || (usableContentType.indexOf('*') != -1)) { - usableContentType = contentResolver.getType(uri); - } - if (usableContentType == null) { - usableContentType = MimeUtility.getMimeTypeByExtension(name); - } - - if (size <= 0) { - String uriString = uri.toString(); - if (uriString.startsWith("file://")) { - Log.v(K9.LOG_TAG, uriString.substring("file://".length())); - File f = new File(uriString.substring("file://".length())); - size = f.length(); - } else { - Log.v(K9.LOG_TAG, "Not a file: " + uriString); - } - } else { - Log.v(K9.LOG_TAG, "old attachment.size: " + size); - } - Log.v(K9.LOG_TAG, "new attachment.size: " + size); - Attachment attachment = new Attachment(); + attachment.state = Attachment.LoadingState.URI_ONLY; attachment.uri = uri; - attachment.contentType = usableContentType; - attachment.name = name; - attachment.size = size; + attachment.contentType = contentType; + attachment.loaderId = ++mMaxLoaderId; + + addAttachmentView(attachment); + + initAttachmentInfoLoader(attachment); + } + + private void initAttachmentInfoLoader(Attachment attachment) { + LoaderManager loaderManager = getSupportLoaderManager(); + Bundle bundle = new Bundle(); + bundle.putParcelable(LOADER_ARG_ATTACHMENT, attachment); + loaderManager.initLoader(attachment.loaderId, bundle, mAttachmentInfoLoaderCallback); + } + + private void initAttachmentContentLoader(Attachment attachment) { + LoaderManager loaderManager = getSupportLoaderManager(); + Bundle bundle = new Bundle(); + bundle.putParcelable(LOADER_ARG_ATTACHMENT, attachment); + loaderManager.initLoader(attachment.loaderId, bundle, mAttachmentContentLoaderCallback); + } + + private void addAttachmentView(Attachment attachment) { + boolean hasMetadata = (attachment.state != Attachment.LoadingState.URI_ONLY); + boolean isLoadingComplete = (attachment.state == Attachment.LoadingState.COMPLETE); View view = getLayoutInflater().inflate(R.layout.message_compose_attachment, mAttachments, false); - TextView nameView = (TextView)view.findViewById(R.id.attachment_name); - ImageButton delete = (ImageButton)view.findViewById(R.id.attachment_delete); - nameView.setText(attachment.name); - delete.setOnClickListener(this); + TextView nameView = (TextView) view.findViewById(R.id.attachment_name); + View progressBar = view.findViewById(R.id.progressBar); + + if (hasMetadata) { + nameView.setText(attachment.name); + } else { + nameView.setText(R.string.loading_attachment); + } + + progressBar.setVisibility(isLoadingComplete ? View.GONE : View.VISIBLE); + + ImageButton delete = (ImageButton) view.findViewById(R.id.attachment_delete); + delete.setOnClickListener(MessageCompose.this); delete.setTag(view); + view.setTag(attachment); mAttachments.addView(view); } + private View getAttachmentView(int loaderId) { + for (int i = 0, childCount = mAttachments.getChildCount(); i < childCount; i++) { + View view = mAttachments.getChildAt(i); + Attachment tag = (Attachment) view.getTag(); + if (tag != null && tag.loaderId == loaderId) { + return view; + } + } + + return null; + } + + private LoaderManager.LoaderCallbacks mAttachmentInfoLoaderCallback = + new LoaderManager.LoaderCallbacks() { + @Override + public Loader onCreateLoader(int id, Bundle args) { + onFetchAttachmentStarted(); + Attachment attachment = args.getParcelable(LOADER_ARG_ATTACHMENT); + return new AttachmentInfoLoader(MessageCompose.this, attachment); + } + + @Override + public void onLoadFinished(Loader loader, Attachment attachment) { + int loaderId = loader.getId(); + + View view = getAttachmentView(loaderId); + if (view != null) { + view.setTag(attachment); + + TextView nameView = (TextView) view.findViewById(R.id.attachment_name); + nameView.setText(attachment.name); + + attachment.loaderId = ++mMaxLoaderId; + initAttachmentContentLoader(attachment); + } else { + onFetchAttachmentFinished(); + } + + getSupportLoaderManager().destroyLoader(loaderId); + } + + @Override + public void onLoaderReset(Loader loader) { + onFetchAttachmentFinished(); + } + }; + + private LoaderManager.LoaderCallbacks mAttachmentContentLoaderCallback = + new LoaderManager.LoaderCallbacks() { + @Override + public Loader onCreateLoader(int id, Bundle args) { + Attachment attachment = args.getParcelable(LOADER_ARG_ATTACHMENT); + return new AttachmentContentLoader(MessageCompose.this, attachment); + } + + @Override + public void onLoadFinished(Loader loader, Attachment attachment) { + int loaderId = loader.getId(); + + View view = getAttachmentView(loaderId); + if (view != null) { + if (attachment.state == Attachment.LoadingState.COMPLETE) { + view.setTag(attachment); + + View progressBar = view.findViewById(R.id.progressBar); + progressBar.setVisibility(View.GONE); + } else { + mAttachments.removeView(view); + } + } + + onFetchAttachmentFinished(); + + getSupportLoaderManager().destroyLoader(loaderId); + } + + @Override + public void onLoaderReset(Loader loader) { + onFetchAttachmentFinished(); + } + }; + + private void onFetchAttachmentStarted() { + mNumAttachmentsLoading += 1; + } + + private void onFetchAttachmentFinished() { + // We're not allowed to perform fragment transactions when called from onLoadFinished(). + // So we use the Handler to call performStalledAction(). + mHandler.sendEmptyMessage(MSG_PERFORM_STALLED_ACTION); + } + + private void performStalledAction() { + mNumAttachmentsLoading -= 1; + + WaitingAction waitingFor = mWaitingForAttachments; + mWaitingForAttachments = WaitingAction.NONE; + + if (waitingFor != WaitingAction.NONE) { + dismissWaitingForAttachmentDialog(); + } + + switch (waitingFor) { + case SEND: { + performSend(); + break; + } + case SAVE: { + performSave(); + break; + } + } + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // if a CryptoSystem activity is returning, then mPreventDraftSaving was set to true @@ -2316,6 +2493,45 @@ public class MessageCompose extends K9Activity implements OnClickListener { } } + private void showWaitingForAttachmentDialog() { + String title; + + switch (mWaitingForAttachments) { + case SEND: { + title = getString(R.string.fetching_attachment_dialog_title_send); + break; + } + case SAVE: { + title = getString(R.string.fetching_attachment_dialog_title_save); + break; + } + default: { + return; + } + } + + ProgressDialogFragment fragment = ProgressDialogFragment.newInstance(title, + getString(R.string.fetching_attachment_dialog_message)); + fragment.show(getSupportFragmentManager(), FRAGMENT_WAITING_FOR_ATTACHMENT); + } + + public void onCancel(ProgressDialogFragment fragment) { + attachmentProgressDialogCancelled(); + } + + void attachmentProgressDialogCancelled() { + mWaitingForAttachments = WaitingAction.NONE; + } + + private void dismissWaitingForAttachmentDialog() { + ProgressDialogFragment fragment = (ProgressDialogFragment) + getSupportFragmentManager().findFragmentByTag(FRAGMENT_WAITING_FOR_ATTACHMENT); + + if (fragment != null) { + fragment.dismiss(); + } + } + @Override public Dialog onCreateDialog(int id) { switch (id) { diff --git a/src/com/fsck/k9/activity/loader/AttachmentContentLoader.java b/src/com/fsck/k9/activity/loader/AttachmentContentLoader.java new file mode 100644 index 000000000..7d677ecbc --- /dev/null +++ b/src/com/fsck/k9/activity/loader/AttachmentContentLoader.java @@ -0,0 +1,81 @@ +package com.fsck.k9.activity.loader; + +import android.content.Context; +import android.support.v4.content.AsyncTaskLoader; +import android.util.Log; + +import com.fsck.k9.K9; +import com.fsck.k9.activity.misc.Attachment; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Loader to fetch the content of an attachment. + * + * This will copy the data to a temporary file in our app's cache directory. + */ +public class AttachmentContentLoader extends AsyncTaskLoader { + private static final String FILENAME_PREFIX = "attachment"; + + private final Attachment mAttachment; + + public AttachmentContentLoader(Context context, Attachment attachment) { + super(context); + mAttachment = attachment; + } + + @Override + protected void onStartLoading() { + if (mAttachment.state == Attachment.LoadingState.COMPLETE) { + deliverResult(mAttachment); + } + + if (takeContentChanged() || mAttachment.state == Attachment.LoadingState.METADATA) { + forceLoad(); + } + } + + @Override + public Attachment loadInBackground() { + Context context = getContext(); + + try { + File file = File.createTempFile(FILENAME_PREFIX, null, context.getCacheDir()); + file.deleteOnExit(); + + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Saving attachment to " + file.getAbsolutePath()); + } + + InputStream in = context.getContentResolver().openInputStream(mAttachment.uri); + try { + FileOutputStream out = new FileOutputStream(file); + try { + IOUtils.copy(in, out); + } finally { + out.close(); + } + } finally { + in.close(); + } + + mAttachment.filename = file.getAbsolutePath(); + mAttachment.state = Attachment.LoadingState.COMPLETE; + + return mAttachment; + } catch (IOException e) { + e.printStackTrace(); + } + + mAttachment.filename = null; + mAttachment.state = Attachment.LoadingState.CANCELLED; + + return mAttachment; + } +} diff --git a/src/com/fsck/k9/activity/loader/AttachmentInfoLoader.java b/src/com/fsck/k9/activity/loader/AttachmentInfoLoader.java new file mode 100644 index 000000000..74c874ae8 --- /dev/null +++ b/src/com/fsck/k9/activity/loader/AttachmentInfoLoader.java @@ -0,0 +1,100 @@ +package com.fsck.k9.activity.loader; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.OpenableColumns; +import android.support.v4.content.AsyncTaskLoader; +import android.util.Log; + +import com.fsck.k9.K9; +import com.fsck.k9.activity.misc.Attachment; +import com.fsck.k9.mail.internet.MimeUtility; + +import java.io.File; + +/** + * Loader to fetch metadata of an attachment. + */ +public class AttachmentInfoLoader extends AsyncTaskLoader { + private final Attachment mAttachment; + + public AttachmentInfoLoader(Context context, Attachment attachment) { + super(context); + mAttachment = attachment; + } + + @Override + protected void onStartLoading() { + if (mAttachment.state == Attachment.LoadingState.METADATA) { + deliverResult(mAttachment); + } + + if (takeContentChanged() || mAttachment.state == Attachment.LoadingState.URI_ONLY) { + forceLoad(); + } + } + + @Override + public Attachment loadInBackground() { + Uri uri = mAttachment.uri; + String contentType = mAttachment.contentType; + + long size = -1; + String name = null; + + ContentResolver contentResolver = getContext().getContentResolver(); + + Cursor metadataCursor = contentResolver.query( + uri, + new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }, + null, + null, + null); + + if (metadataCursor != null) { + try { + if (metadataCursor.moveToFirst()) { + name = metadataCursor.getString(0); + size = metadataCursor.getInt(1); + } + } finally { + metadataCursor.close(); + } + } + + if (name == null) { + name = uri.getLastPathSegment(); + } + + String usableContentType = contentType; + if ((usableContentType == null) || (usableContentType.indexOf('*') != -1)) { + usableContentType = contentResolver.getType(uri); + } + if (usableContentType == null) { + usableContentType = MimeUtility.getMimeTypeByExtension(name); + } + + if (size <= 0) { + String uriString = uri.toString(); + if (uriString.startsWith("file://")) { + Log.v(K9.LOG_TAG, uriString.substring("file://".length())); + File f = new File(uriString.substring("file://".length())); + size = f.length(); + } else { + Log.v(K9.LOG_TAG, "Not a file: " + uriString); + } + } else { + Log.v(K9.LOG_TAG, "old attachment.size: " + size); + } + Log.v(K9.LOG_TAG, "new attachment.size: " + size); + + mAttachment.contentType = usableContentType; + mAttachment.name = name; + mAttachment.size = size; + mAttachment.state = Attachment.LoadingState.METADATA; + + return mAttachment; + } +} diff --git a/src/com/fsck/k9/activity/misc/Attachment.java b/src/com/fsck/k9/activity/misc/Attachment.java new file mode 100644 index 000000000..e64165783 --- /dev/null +++ b/src/com/fsck/k9/activity/misc/Attachment.java @@ -0,0 +1,129 @@ +package com.fsck.k9.activity.misc; + +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Container class for information about an attachment. + * + * This is used by {@link com.fsck.k9.activity.MessageCompose} to fetch and manage attachments. + */ +public class Attachment implements Parcelable { + /** + * The URI pointing to the source of the attachment. + * + * In most cases this will be a {@code content://}-URI. + */ + public Uri uri; + + /** + * The current loading state. + */ + public LoadingState state; + + /** + * The ID of the loader that is used to load the metadata or contents. + */ + public int loaderId; + + /** + * The content type of the attachment. + * + * Only valid when {@link #state} is {@link LoadingState#METADATA} or + * {@link LoadingState#COMPLETE}. + */ + public String contentType; + + /** + * The (file)name of the attachment. + * + * Only valid when {@link #state} is {@link LoadingState#METADATA} or + * {@link LoadingState#COMPLETE}. + */ + public String name; + + /** + * The size of the attachment. + * + * Only valid when {@link #state} is {@link LoadingState#METADATA} or + * {@link LoadingState#COMPLETE}. + */ + public long size; + + /** + * The name of the temporary file containing the local copy of the attachment. + * + * Only valid when {@link #state} is {@link LoadingState#COMPLETE}. + */ + public String filename; + + + public Attachment() {} + + public static enum LoadingState { + /** + * The only thing we know about this attachment is {@link #uri}. + */ + URI_ONLY, + + /** + * The metadata of this attachment have been loaded. + * + * {@link #contentType}, {@link #name}, and {@link #size} should contain usable values. + */ + METADATA, + + /** + * The contents of the attachments have been copied to the temporary file {@link #filename}. + */ + COMPLETE, + + /** + * Something went wrong while trying to fetch the attachment's contents. + */ + CANCELLED + } + + + // === Parcelable === + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(uri, flags); + dest.writeSerializable(state); + dest.writeInt(loaderId); + dest.writeString(contentType); + dest.writeString(name); + dest.writeLong(size); + dest.writeString(filename); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public Attachment createFromParcel(Parcel in) { + return new Attachment(in); + } + + @Override + public Attachment[] newArray(int size) { + return new Attachment[size]; + } + }; + + public Attachment(Parcel in) { + uri = in.readParcelable(Uri.class.getClassLoader()); + state = (LoadingState) in.readSerializable(); + loaderId = in.readInt(); + contentType = in.readString(); + name = in.readString(); + size = in.readLong(); + filename = in.readString(); + } +} diff --git a/src/com/fsck/k9/fragment/MessageViewFragment.java b/src/com/fsck/k9/fragment/MessageViewFragment.java index d2bd16d89..711fcb93d 100644 --- a/src/com/fsck/k9/fragment/MessageViewFragment.java +++ b/src/com/fsck/k9/fragment/MessageViewFragment.java @@ -752,8 +752,8 @@ public class MessageViewFragment extends SherlockFragment implements OnClickList break; } case R.id.dialog_attachment_progress: { - String title = getString(R.string.dialog_attachment_progress_title); - fragment = ProgressDialogFragment.newInstance(title); + String message = getString(R.string.dialog_attachment_progress_title); + fragment = ProgressDialogFragment.newInstance(null, message); break; } default: { diff --git a/src/com/fsck/k9/fragment/ProgressDialogFragment.java b/src/com/fsck/k9/fragment/ProgressDialogFragment.java index 671758a86..467bbcc83 100644 --- a/src/com/fsck/k9/fragment/ProgressDialogFragment.java +++ b/src/com/fsck/k9/fragment/ProgressDialogFragment.java @@ -2,19 +2,22 @@ package com.fsck.k9.fragment; import android.app.Dialog; import android.app.ProgressDialog; +import android.content.DialogInterface; import android.os.Bundle; import com.actionbarsherlock.app.SherlockDialogFragment; public class ProgressDialogFragment extends SherlockDialogFragment { - private static final String ARG_TITLE = "title"; + protected static final String ARG_TITLE = "title"; + protected static final String ARG_MESSAGE = "message"; - public static ProgressDialogFragment newInstance(String title) { + public static ProgressDialogFragment newInstance(String title, String message) { ProgressDialogFragment fragment = new ProgressDialogFragment(); Bundle args = new Bundle(); args.putString(ARG_TITLE, title); + args.putString(ARG_MESSAGE, message); fragment.setArguments(args); return fragment; @@ -25,11 +28,28 @@ public class ProgressDialogFragment extends SherlockDialogFragment { public Dialog onCreateDialog(Bundle savedInstanceState) { Bundle args = getArguments(); String title = args.getString(ARG_TITLE); + String message = args.getString(ARG_MESSAGE); ProgressDialog dialog = new ProgressDialog(getActivity()); dialog.setIndeterminate(true); dialog.setTitle(title); + dialog.setMessage(message); return dialog; } + + @Override + public void onCancel(DialogInterface dialog) { + CancelListener listener = (CancelListener) getActivity(); + if (listener != null && listener instanceof CancelListener) { + listener.onCancel(this); + } + + super.onCancel(dialog); + } + + + public interface CancelListener { + void onCancel(ProgressDialogFragment fragment); + } } diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index f05ca59ac..37c5b5570 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -3,6 +3,7 @@ package com.fsck.k9.mail.store; import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; @@ -93,6 +94,7 @@ public class LocalStore extends Store implements Serializable { private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0]; private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0]; + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; /* * a String containing the columns getMessages expects to work with @@ -4002,11 +4004,67 @@ public class LocalStore extends Store implements Serializable { } } - public static class LocalAttachmentBody implements Body { - private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + public abstract static class BinaryAttachmentBody implements Body { + protected String mEncoding; + + @Override + public abstract InputStream getInputStream() throws MessagingException; + + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + InputStream in = getInputStream(); + try { + boolean closeStream = false; + if (MimeUtil.isBase64Encoding(mEncoding)) { + out = new Base64OutputStream(out); + closeStream = true; + } else if (MimeUtil.isQuotedPrintableEncoded(mEncoding)){ + out = new QuotedPrintableOutputStream(out, false); + closeStream = true; + } + + try { + IOUtils.copy(in, out); + } finally { + if (closeStream) { + out.close(); + } + } + } finally { + in.close(); + } + } + + @Override + public void setEncoding(String encoding) throws MessagingException { + mEncoding = encoding; + } + + public String getEncoding() { + return mEncoding; + } + } + + public static class TempFileBody extends BinaryAttachmentBody { + private final File mFile; + + public TempFileBody(String filename) { + mFile = new File(filename); + } + + @Override + public InputStream getInputStream() throws MessagingException { + try { + return new FileInputStream(mFile); + } catch (FileNotFoundException e) { + return new ByteArrayInputStream(EMPTY_BYTE_ARRAY); + } + } + } + + public static class LocalAttachmentBody extends BinaryAttachmentBody { private Application mApplication; private Uri mUri; - protected String mEncoding; public LocalAttachmentBody(Uri uri, Application application) { mApplication = application; @@ -4054,10 +4112,6 @@ public class LocalStore extends Store implements Serializable { public Uri getContentUri() { return mUri; } - - public void setEncoding(String encoding) throws MessagingException { - mEncoding = encoding; - } } /** @@ -4071,25 +4125,8 @@ public class LocalStore extends Store implements Serializable { } @Override - public void writeTo(OutputStream out) throws IOException, - MessagingException { - InputStream in = getInputStream(); - try { - if (MimeUtil.ENC_7BIT.equalsIgnoreCase(mEncoding)) { - /* - * If we knew the message was already 7bit clean, then it - * could be sent along without processing. But since we - * don't know, we recursively parse it. - */ - MimeMessage message = new MimeMessage(in, true); - message.setUsing7bitTransport(); - message.writeTo(out); - } else { - IOUtils.copy(in, out); - } - } finally { - in.close(); - } + public void writeTo(OutputStream out) throws IOException, MessagingException { + AttachmentMessageBodyUtil.writeTo(this, out); } @Override @@ -4114,6 +4151,56 @@ public class LocalStore extends Store implements Serializable { } } + public static class TempFileMessageBody extends TempFileBody implements CompositeBody { + + public TempFileMessageBody(String filename) { + super(filename); + } + + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + AttachmentMessageBodyUtil.writeTo(this, out); + } + + @Override + public void setUsing7bitTransport() throws MessagingException { + // see LocalAttachmentMessageBody.setUsing7bitTransport() + } + + @Override + public void setEncoding(String encoding) throws MessagingException { + if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding) + && !MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) { + throw new MessagingException( + "Incompatible content-transfer-encoding applied to a CompositeBody"); + } + mEncoding = encoding; + } + } + + public static class AttachmentMessageBodyUtil { + public static void writeTo(BinaryAttachmentBody body, OutputStream out) throws IOException, + MessagingException { + InputStream in = body.getInputStream(); + try { + if (MimeUtil.ENC_7BIT.equalsIgnoreCase(body.getEncoding())) { + /* + * If we knew the message was already 7bit clean, then it + * could be sent along without processing. But since we + * don't know, we recursively parse it. + */ + MimeMessage message = new MimeMessage(in, true); + message.setUsing7bitTransport(); + message.writeTo(out); + } else { + IOUtils.copy(in, out); + } + } finally { + in.close(); + } + } + } + static class ThreadInfo { public final long threadId; public final long msgId;