/* Copyright (C) 2011 The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.exchange.adapter; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.net.Uri; import android.os.RemoteException; import com.android.emailcommon.provider.EmailContent.Attachment; import com.android.emailcommon.provider.EmailContent.AttachmentColumns; import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.service.EmailServiceStatus; import com.android.emailcommon.utility.AttachmentUtilities; import com.android.exchange.Eas; import com.android.exchange.EasResponse; import com.android.exchange.EasSyncService; import com.android.exchange.ExchangeService; import com.android.exchange.PartRequest; import com.android.exchange.utility.UriCodec; import com.google.common.annotations.VisibleForTesting; import org.apache.http.HttpStatus; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Handle EAS attachment loading, regardless of protocol version */ public class AttachmentLoader { static private final int CHUNK_SIZE = 16*1024; private final EasSyncService mService; private final Context mContext; private final ContentResolver mResolver; private final Attachment mAttachment; private final long mAttachmentId; private final int mAttachmentSize; private final long mMessageId; private final Message mMessage; private final long mAccountId; private final Uri mAttachmentUri; public AttachmentLoader(EasSyncService service, PartRequest req) { mService = service; mContext = service.mContext; mResolver = service.mContentResolver; mAttachment = req.mAttachment; mAttachmentId = mAttachment.mId; mAttachmentSize = (int)mAttachment.mSize; mAccountId = mAttachment.mAccountKey; mMessageId = mAttachment.mMessageKey; mMessage = Message.restoreMessageWithId(mContext, mMessageId); mAttachmentUri = AttachmentUtilities.getAttachmentUri(mAccountId, mAttachmentId); } private void doStatusCallback(int status) { try { ExchangeService.callback().loadAttachmentStatus(mMessageId, mAttachmentId, status, 0); } catch (RemoteException e) { // No danger if the client is no longer around } } private void doProgressCallback(int progress) { try { ExchangeService.callback().loadAttachmentStatus(mMessageId, mAttachmentId, EmailServiceStatus.IN_PROGRESS, progress); } catch (RemoteException e) { // No danger if the client is no longer around } } /** * Save away the contentUri for this Attachment and notify listeners */ private void finishLoadAttachment() { ContentValues cv = new ContentValues(); cv.put(AttachmentColumns.CONTENT_URI, mAttachmentUri.toString()); mAttachment.update(mContext, cv); doStatusCallback(EmailServiceStatus.SUCCESS); } /** * Read the attachment data in chunks and write the data back out to our attachment file * @param inputStream the InputStream we're reading the attachment from * @param outputStream the OutputStream the attachment will be written to * @param len the number of expected bytes we're going to read * @throws IOException */ public void readChunked(InputStream inputStream, OutputStream outputStream, int len) throws IOException { byte[] bytes = new byte[CHUNK_SIZE]; int length = len; // Loop terminates 1) when EOF is reached or 2) IOException occurs // One of these is guaranteed to occur int totalRead = 0; int lastCallbackPct = -1; int lastCallbackTotalRead = 0; mService.userLog("Expected attachment length: ", len); while (true) { int read = inputStream.read(bytes, 0, CHUNK_SIZE); if (read < 0) { // -1 means EOF mService.userLog("Attachment load reached EOF, totalRead: ", totalRead); break; } // Keep track of how much we've read for progress callback totalRead += read; // Write these bytes out outputStream.write(bytes, 0, read); // We can't report percentage if data is chunked; the length of incoming data is unknown if (length > 0) { int pct = (totalRead * 100) / length; // Callback only if we've read at least 1% more and have read more than CHUNK_SIZE // We don't want to spam the Email app if ((pct > lastCallbackPct) && (totalRead > (lastCallbackTotalRead + CHUNK_SIZE))) { // Report progress back to the UI doProgressCallback(pct); lastCallbackTotalRead = totalRead; lastCallbackPct = pct; } } } if (totalRead > length) { // Apparently, the length, as reported by EAS, isn't always accurate; let's log it mService.userLog("Read more than expected: ", totalRead); } } @VisibleForTesting static String encodeForExchange2003(String str) { AttachmentNameEncoder enc = new AttachmentNameEncoder(); StringBuilder sb = new StringBuilder(str.length() + 16); enc.appendPartiallyEncoded(sb, str); return sb.toString(); } /** * 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(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 == '.'; } } /** * Loads an attachment, based on the PartRequest passed in the constructor * @throws IOException */ public void loadAttachment() throws IOException { if (mMessage == null) { doStatusCallback(EmailServiceStatus.MESSAGE_NOT_FOUND); return; } // Say we've started loading the attachment doProgressCallback(0); EasResponse resp; boolean eas14 = mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE; // The method of attachment loading is different in EAS 14.0 than in earlier versions if (eas14) { 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, mAttachment.mLocation); s.end().end().done(); // ITEMS_FETCH, ITEMS_ITEMS resp = mService.sendHttpClientPost("ItemOperations", s.toByteArray()); } else { String location = mAttachment.mLocation; // For Exchange 2003 (EAS 2.5), we have to look for illegal characters in the file name // that EAS sent to us! if (mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { location = encodeForExchange2003(location); } String cmd = "GetAttachment&AttachmentName=" + location; resp = mService.sendHttpClientPost(cmd, null, EasSyncService.COMMAND_TIMEOUT); } try { int status = resp.getStatus(); if (status == HttpStatus.SC_OK) { if (!resp.isEmpty()) { InputStream is = resp.getInputStream(); OutputStream os = null; try { os = mResolver.openOutputStream(mAttachmentUri); if (eas14) { ItemOperationsParser p = new ItemOperationsParser(this, is, os, mAttachmentSize); p.parse(); if (p.getStatusCode() == 1 /* Success */) { finishLoadAttachment(); return; } } else { int len = resp.getLength(); if (len != 0) { // len > 0 means that Content-Length was set in the headers // len < 0 means "chunked" transfer-encoding readChunked(is, os, (len < 0) ? mAttachmentSize : len); finishLoadAttachment(); return; } } } catch (FileNotFoundException e) { mService.errorLog("Can't get attachment; write file not found?"); } finally { if (os != null) { os.flush(); os.close(); } } } } } finally { resp.close(); } // All errors lead here... doStatusCallback(EmailServiceStatus.ATTACHMENT_NOT_FOUND); } }