1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-27 11:42:16 -05:00

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
This commit is contained in:
cketti 2013-08-14 18:05:57 +02:00
parent 8d0f697e36
commit 62aa1b87d0
8 changed files with 548 additions and 113 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

@ -1147,4 +1147,6 @@ Please submit bug reports, contribute new features and ask questions at
<string name="preposition_for_date">on <xliff:g id="date">%s</xliff:g></string>
<string name="mark_all_as_read">Mark all as read</string>
<string name="loading_attachment">Loading 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,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<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.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<Parcelable> attachments = savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS);
mAttachments.removeAllViews();
for (Parcelable p : attachments) {
Uri uri = (Uri) p;
addAttachment(uri);
mMaxLoaderId = 0;
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
@ -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<Attachment> mAttachmentInfoLoaderCallback =
new LoaderManager.LoaderCallbacks<Attachment>() {
@Override
public Loader<Attachment> onCreateLoader(int id, Bundle args) {
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);
}
getSupportLoaderManager().destroyLoader(loaderId);
}
@Override
public void onLoaderReset(Loader<Attachment> loader) {
}
};
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);
}
}
getSupportLoaderManager().destroyLoader(loaderId);
}
@Override
public void onLoaderReset(Loader<Attachment> loader) {
}
};
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// if a CryptoSystem activity is returning, then mPreventDraftSaving was set to true

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

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