From 62aa1b87d0b313355970dee69427bd93f5b84b28 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 14 Aug 2013 18:05:57 +0200 Subject: [PATCH] Fetch attachments while MessageCompose activity is running Android allows other apps to access protected content of an app without requesting the necessary permission when the app returns an Intent with FLAG_GRANT_READ_URI_PERMISSION. This regularly happens as a result of ACTION_GET_CONTENT, i.e. what we use to pick content to be attached to a message. Accessing that content only works while the receiving activity is running. Afterwards accessing the content throws a SecurityException because of the missing permission. This commit changes K-9 Mail's behavior to copy the content to a temporary file in K-9's cache directory while the activity is still running. Fixes issue 4847, 5821 This also fixes bugs related to the fact that K-9 Mail didn't save a copy of attached content in the message database. Fixes issue 1187, 3330, 4930 --- res/layout/message_compose_attachment.xml | 85 ++++--- res/values/strings.xml | 2 + src/com/fsck/k9/activity/K9Activity.java | 4 +- src/com/fsck/k9/activity/MessageCompose.java | 225 ++++++++++++------ .../loader/AttachmentContentLoader.java | 81 +++++++ .../activity/loader/AttachmentInfoLoader.java | 100 ++++++++ src/com/fsck/k9/activity/misc/Attachment.java | 129 ++++++++++ src/com/fsck/k9/mail/store/LocalStore.java | 35 ++- 8 files changed, 548 insertions(+), 113 deletions(-) create mode 100644 src/com/fsck/k9/activity/loader/AttachmentContentLoader.java create mode 100644 src/com/fsck/k9/activity/loader/AttachmentInfoLoader.java create mode 100644 src/com/fsck/k9/activity/misc/Attachment.java 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 b306f4420..fd4bd3f0f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1147,4 +1147,6 @@ Please submit bug reports, contribute new features and ask questions at on %s Mark all as read + + Loading 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 5d9d3759a..999bb6947 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,6 +54,9 @@ 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; @@ -86,14 +88,13 @@ 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; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -147,6 +148,8 @@ public class MessageCompose extends K9Activity implements OnClickListener { private static final String STATE_KEY_QUOTED_TEXT_FORMAT = "com.fsck.k9.activity.MessageCompose.quotedTextFormat"; + private static final String LOADER_ARG_ATTACHMENT = "attachment"; + private static final int MSG_PROGRESS_ON = 1; private static final int MSG_PROGRESS_OFF = 2; private static final int MSG_SKIPPED_ATTACHMENTS = 3; @@ -218,6 +221,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, @@ -365,14 +369,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. @@ -1077,12 +1073,13 @@ 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.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); @@ -1104,11 +1101,22 @@ 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; + + 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 @@ -1474,8 +1482,11 @@ public class MessageCompose extends K9Activity implements OnClickListener { for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag(); - MimeBodyPart bp = new MimeBodyPart( - new LocalStore.LocalAttachmentBody(attachment.uri, getApplication())); + if (attachment.state != Attachment.LoadingState.COMPLETE) { + continue; + } + + MimeBodyPart bp = new MimeBodyPart(new LocalStore.TempFileBody(attachment.filename)); /* * Correctly encode the filename here. Otherwise the whole @@ -1911,71 +1922,131 @@ 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) { + 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); + } + + getSupportLoaderManager().destroyLoader(loaderId); + } + + @Override + public void onLoaderReset(Loader loader) { + } + }; + + 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); + } + } + + getSupportLoaderManager().destroyLoader(loaderId); + } + + @Override + public void onLoaderReset(Loader loader) { + } + }; + + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // if a CryptoSystem activity is returning, then mPreventDraftSaving was set to true 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/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 5575499a7..de1c4272e 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; @@ -90,6 +91,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 @@ -3984,8 +3986,39 @@ public class LocalStore extends Store implements Serializable { } } + public static class TempFileBody implements Body { + 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); + } + } + + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + InputStream in = getInputStream(); + try { + Base64OutputStream base64Out = new Base64OutputStream(out); + try { + IOUtils.copy(in, base64Out); + } finally { + base64Out.close(); + } + } finally { + in.close(); + } + } + } + public static class LocalAttachmentBody implements Body { - private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; private Application mApplication; private Uri mUri;