Merge branch 'pick_attachment_fix'

Update LocalStore code to handle the newly introduced temporary files
for attachments

Conflicts:
	res/values/strings.xml
	src/com/fsck/k9/activity/MessageCompose.java
This commit is contained in:
cketti 2013-09-25 03:54:35 +02:00
commit 2b7f5e7b70
10 changed files with 803 additions and 146 deletions

View File

@ -1,36 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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">
<ImageButton
android:id="@+id/attachment_delete"
android:src="@drawable/ic_delete"
android:layout_alignParentRight="true"
android:layout_height="42dip"
android:layout_width="42dip" />
<TextView
android:id="@+id/attachment_name"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorSecondary"
android:layout_width="1dip"
android:layout_height="42dip"
android:background="?attr/messageViewAttachmentBackground"
android:paddingLeft="36dip"
android:singleLine="true"
android:ellipsize="start"
android:gravity="center_vertical"
android:layout_marginLeft="6dip"
android:layout_marginRight="4dip"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@id/attachment_delete" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_email_attachment"
android:layout_marginLeft="1dip"
android:layout_centerVertical="true" />
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">
<ImageButton
android:id="@+id/attachment_delete"
android:src="@drawable/ic_delete"
android:layout_alignParentRight="true"
android:layout_height="42dip"
android:layout_width="42dip" />
<LinearLayout
android:layout_width="1dip"
android:layout_height="42dip"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@id/attachment_delete"
android:layout_marginLeft="6dip"
android:layout_marginRight="4dip"
android:paddingLeft="36dip"
android:gravity="center_vertical"
android:background="?attr/messageViewAttachmentBackground"
android:orientation="horizontal">
<TextView
android:id="@+id/attachment_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorSecondary"
android:singleLine="true"
android:ellipsize="start"/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleSmall"
android:layout_width="32dp"
android:layout_height="fill_parent"
android:layout_marginLeft="4dp"/>
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_email_attachment"
android:layout_marginLeft="1dip"
android:layout_centerVertical="true" />
</RelativeLayout>

View File

@ -1152,4 +1152,9 @@ Please submit bug reports, contribute new features and ask questions at
<string name="global_settings_messageview_visible_refile_actions_title">Visible message actions</string>
<string name="global_settings_messageview_visible_refile_actions_summary">Show selected actions in the message view menu</string>
<string name="loading_attachment">Loading attachment…</string>
<string name="fetching_attachment_dialog_title_send">Sending message</string>
<string name="fetching_attachment_dialog_title_save">Saving draft</string>
<string name="fetching_attachment_dialog_message">Fetching attachment…</string>
</resources>

View File

@ -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;

View File

@ -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<Uri> attachments = new ArrayList<Uri>();
ArrayList<Attachment> attachments = new ArrayList<Attachment>();
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<Parcelable> 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<Attachment> 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<Attachment> mAttachmentInfoLoaderCallback =
new LoaderManager.LoaderCallbacks<Attachment>() {
@Override
public Loader<Attachment> onCreateLoader(int id, Bundle args) {
onFetchAttachmentStarted();
Attachment attachment = args.getParcelable(LOADER_ARG_ATTACHMENT);
return new AttachmentInfoLoader(MessageCompose.this, attachment);
}
@Override
public void onLoadFinished(Loader<Attachment> 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<Attachment> loader) {
onFetchAttachmentFinished();
}
};
private LoaderManager.LoaderCallbacks<Attachment> mAttachmentContentLoaderCallback =
new LoaderManager.LoaderCallbacks<Attachment>() {
@Override
public Loader<Attachment> onCreateLoader(int id, Bundle args) {
Attachment attachment = args.getParcelable(LOADER_ARG_ATTACHMENT);
return new AttachmentContentLoader(MessageCompose.this, attachment);
}
@Override
public void onLoadFinished(Loader<Attachment> 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<Attachment> 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) {

View File

@ -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<Attachment> {
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;
}
}

View File

@ -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<Attachment> {
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;
}
}

View File

@ -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<Attachment> CREATOR =
new Parcelable.Creator<Attachment>() {
@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();
}
}

View File

@ -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: {

View File

@ -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);
}
}

View File

@ -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;