blob: fe35728b67b0ef60abb5c72ae8d632180066d85d [file] [log] [blame]
package com.android.exchange.service;
import android.content.Context;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.exchange.Eas;
import com.android.exchange.EasResponse;
import com.android.exchange.adapter.ItemOperationsParser;
import com.android.exchange.adapter.Serializer;
import com.android.exchange.adapter.Tags;
import com.android.exchange.utility.UriCodec;
import com.android.mail.utils.LogUtils;
import org.apache.http.HttpStatus;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Loads attachments from the Exchange server.
* TODO: Add ability to call back to UI when this failed, and generally better handle error cases.
*/
public class EasAttachmentLoader extends EasServerConnection {
private static final String TAG = "EasAttachmentLoader";
private EasAttachmentLoader(final Context context, final Account account) {
super(context, account);
}
/**
* Load an attachment from the Exchange server, and write it to the content provider.
* @param context Our {@link Context}.
* @param attachmentId The local id of the attachment (i.e. its id in the database).
*/
public static void loadAttachment(final Context context, final long attachmentId) {
// TODO: This function should do status callbacks when starting and ending the download.
// (Progress updates need to be handled in the chunk loader.)
final Attachment attachment = Attachment.restoreAttachmentWithId(context, attachmentId);
if (attachment == null) {
LogUtils.d(TAG, "Could not load attachment %d", attachmentId);
} else {
final Account account = Account.restoreAccountWithId(context, attachment.mAccountKey);
if (account == null) {
LogUtils.d(TAG, "Attachment %d has bad account key %d", attachment.mId,
attachment.mAccountKey);
// TODO: Purge this attachment?
} else {
final EasAttachmentLoader loader = new EasAttachmentLoader(context, account);
loader.load(attachment);
}
}
}
/**
* Encoder for Exchange 2003 attachment names. They come from the server partially encoded,
* but there are still possible characters that need to be encoded (Why, MSFT, why?)
*/
private static class AttachmentNameEncoder extends UriCodec {
@Override
protected boolean isRetained(final char c) {
// These four characters are commonly received in EAS 2.5 attachment names and are
// valid (verified by testing); we won't encode them
return c == '_' || c == ':' || c == '/' || c == '.';
}
}
/**
* Finish encoding attachment names for Exchange 2003.
* @param str A partially encoded string.
* @return The fully encoded version of str.
*/
private static String encodeForExchange2003(final String str) {
final AttachmentNameEncoder enc = new AttachmentNameEncoder();
final StringBuilder sb = new StringBuilder(str.length() + 16);
enc.appendPartiallyEncoded(sb, str);
return sb.toString();
}
/**
* Make the appropriate Exchange server request for getting the attachment.
* @param attachment The {@link Attachment} we wish to load.
* @return The {@link EasResponse} for the request, or null if we encountered an error.
*/
private EasResponse performServerRequest(final Attachment attachment) {
try {
// The method of attachment loading is different in EAS 14.0 than in earlier versions
final String cmd;
final byte[] bytes;
if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
final Serializer s = new Serializer();
s.start(Tags.ITEMS_ITEMS).start(Tags.ITEMS_FETCH);
s.data(Tags.ITEMS_STORE, "Mailbox");
s.data(Tags.BASE_FILE_REFERENCE, attachment.mLocation);
s.end().end().done(); // ITEMS_FETCH, ITEMS_ITEMS
cmd = "ItemOperations";
bytes = s.toByteArray();
} else {
final String location;
// For Exchange 2003 (EAS 2.5), we have to look for illegal chars in the file name
// that EAS sent to us!
if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
location = encodeForExchange2003(attachment.mLocation);
} else {
location = attachment.mLocation;
}
cmd = "GetAttachment&AttachmentName=" + location;
bytes = null;
}
return sendHttpClientPost(cmd, bytes);
} catch (final IOException e) {
LogUtils.w(TAG, "IOException while loading attachment from server: %s", e.getMessage());
return null;
}
}
/**
* Close, ignoring errors (as during cleanup)
* @param c a Closeable
*/
private static void close(final Closeable c) {
try {
c.close();
} catch (IOException e) {
LogUtils.w(TAG, "IOException while cleaning up attachment: %s", e.getMessage());
}
}
/**
* Save away the contentUri for this Attachment and notify listeners
*/
private boolean finishLoadAttachment(final Attachment attachment, final File file) {
final InputStream in;
try {
in = new FileInputStream(file);
} catch (final FileNotFoundException e) {
// Unlikely, as we just created it successfully, but log it.
LogUtils.e(TAG, "Could not open attachment file: %s", e.getMessage());
return false;
}
AttachmentUtilities.saveAttachment(mContext, in, attachment);
close(in);
return true;
}
/**
* Read the {@link EasResponse} and extract the attachment data, saving it to the provider.
* @param resp The (successful) {@link EasResponse} containing the attachment data.
* @param attachment The {@link Attachment} with the attachment metadata.
* @return Whether we successfully extracted the attachment data.
*/
private boolean handleResponse(final EasResponse resp, final Attachment attachment) {
final File tmpFile;
try {
tmpFile = File.createTempFile("eas_", "tmp", mContext.getCacheDir());
} catch (final IOException e) {
LogUtils.w(TAG, "Could not open temp file: %s", e.getMessage());
return false;
}
try {
final OutputStream os;
try {
os = new FileOutputStream(tmpFile);
} catch (final FileNotFoundException e) {
LogUtils.w(TAG, "Temp file not found: %s", e.getMessage());
return false;
}
try {
final InputStream is = resp.getInputStream();
try {
final boolean success;
if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
final ItemOperationsParser parser = new ItemOperationsParser(is, os,
attachment.mSize);
parser.parse();
success = (parser.getStatusCode() == 1);
} else {
final int length = resp.getLength();
if (length != 0) {
// len > 0 means that Content-Length was set in the headers
// len < 0 means "chunked" transfer-encoding
ItemOperationsParser.readChunked(is, os,
(length < 0) ? attachment.mSize : length);
}
success = true;
}
final boolean finishSuccess;
if (success) {
finishSuccess = finishLoadAttachment(attachment, tmpFile);
} else {
finishSuccess = false;
}
return finishSuccess;
} catch (final IOException e) {
LogUtils.w(TAG, "Error reading attachment: %s", e.getMessage());
return false;
} finally {
close(is);
}
} finally {
close(os);
}
} finally {
tmpFile.delete();
}
}
/**
* Load the attachment from the server.
* @param attachment The {@link Attachment} we wish to load.
* @return Whether or not the load succeeded.
*/
private boolean load(final Attachment attachment) {
final EasResponse resp = performServerRequest(attachment);
if (resp == null) {
return false;
}
try {
if (resp.getStatus() != HttpStatus.SC_OK || resp.isEmpty()) {
return false;
}
return handleResponse(resp, attachment);
} finally {
resp.close();
}
}
}