1 package com.android.exchange.service; 2 3 import android.content.Context; 4 import android.os.RemoteException; 5 6 import com.android.emailcommon.provider.Account; 7 import com.android.emailcommon.provider.EmailContent.Message; 8 import com.android.emailcommon.provider.EmailContent.Attachment; 9 import com.android.emailcommon.service.EmailServiceStatus; 10 import com.android.emailcommon.service.IEmailServiceCallback; 11 import com.android.emailcommon.utility.AttachmentUtilities; 12 import com.android.exchange.Eas; 13 import com.android.exchange.EasResponse; 14 import com.android.exchange.adapter.ItemOperationsParser; 15 import com.android.exchange.adapter.Serializer; 16 import com.android.exchange.adapter.Tags; 17 import com.android.exchange.utility.UriCodec; 18 import com.android.mail.utils.LogUtils; 19 20 import org.apache.http.HttpStatus; 21 22 import java.io.Closeable; 23 import java.io.File; 24 import java.io.FileInputStream; 25 import java.io.FileNotFoundException; 26 import java.io.FileOutputStream; 27 import java.io.IOException; 28 import java.io.InputStream; 29 import java.io.OutputStream; 30 import java.security.cert.CertificateException; 31 32 /** 33 * Loads attachments from the Exchange server. 34 * TODO: Add ability to call back to UI when this failed, and generally better handle error cases. 35 */ 36 public class EasAttachmentLoader extends EasServerConnection { 37 private static final String TAG = Eas.LOG_TAG; 38 39 private final IEmailServiceCallback mCallback; 40 41 private EasAttachmentLoader(final Context context, final Account account, 42 final IEmailServiceCallback callback) { 43 super(context, account); 44 mCallback = callback; 45 } 46 47 // TODO: EmailServiceStatus.ATTACHMENT_NOT_FOUND is heavily used, may need to split that into 48 // different statuses. 49 private static void doStatusCallback(final IEmailServiceCallback callback, 50 final long messageKey, final long attachmentId, final int status, final int progress) { 51 if (callback != null) { 52 try { 53 callback.loadAttachmentStatus(messageKey, attachmentId, status, progress); 54 } catch (final RemoteException e) { 55 LogUtils.e(TAG, "RemoteException in loadAttachment: %s", e.getMessage()); 56 } 57 } 58 } 59 60 /** 61 * Provides the parser with the data it needs to perform the callback. 62 */ 63 public static class ProgressCallback { 64 private final IEmailServiceCallback mCallback; 65 private final Attachment mAttachment; 66 67 public ProgressCallback(final IEmailServiceCallback callback, 68 final Attachment attachment) { 69 mCallback = callback; 70 mAttachment = attachment; 71 } 72 73 public void doCallback(final int progress) { 74 doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachment.mId, 75 EmailServiceStatus.IN_PROGRESS, progress); 76 } 77 } 78 79 /** 80 * Load an attachment from the Exchange server, and write it to the content provider. 81 * @param context Our {@link Context}. 82 * @param attachmentId The local id of the attachment (i.e. its id in the database). 83 * @param callback The callback for any status updates. 84 */ 85 public static void loadAttachment(final Context context, final long attachmentId, 86 final IEmailServiceCallback callback) { 87 final Attachment attachment = Attachment.restoreAttachmentWithId(context, attachmentId); 88 if (attachment == null) { 89 LogUtils.d(TAG, "Could not load attachment %d", attachmentId); 90 doStatusCallback(callback, -1, attachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND, 91 0); 92 return; 93 } 94 if (attachment.mLocation == null) { 95 LogUtils.e(TAG, "Attachment %d lacks a location", attachmentId); 96 doStatusCallback(callback, -1, attachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND, 97 0); 98 return; 99 } 100 final Account account = Account.restoreAccountWithId(context, attachment.mAccountKey); 101 if (account == null) { 102 LogUtils.d(TAG, "Attachment %d has bad account key %d", attachment.mId, 103 attachment.mAccountKey); 104 doStatusCallback(callback, attachment.mMessageKey, attachmentId, 105 EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0); 106 return; 107 } 108 final Message message = Message.restoreMessageWithId(context, attachment.mMessageKey); 109 if (message == null) { 110 doStatusCallback(callback, attachment.mMessageKey, attachmentId, 111 EmailServiceStatus.MESSAGE_NOT_FOUND, 0); 112 return; 113 } 114 115 // Error cases handled, do the load. 116 final EasAttachmentLoader loader = 117 new EasAttachmentLoader(context, account, callback); 118 final int status = loader.load(attachment); 119 doStatusCallback(callback, attachment.mMessageKey, attachmentId, status, 0); 120 } 121 122 /** 123 * Encoder for Exchange 2003 attachment names. They come from the server partially encoded, 124 * but there are still possible characters that need to be encoded (Why, MSFT, why?) 125 */ 126 private static class AttachmentNameEncoder extends UriCodec { 127 @Override 128 protected boolean isRetained(final char c) { 129 // These four characters are commonly received in EAS 2.5 attachment names and are 130 // valid (verified by testing); we won't encode them 131 return c == '_' || c == ':' || c == '/' || c == '.'; 132 } 133 } 134 135 /** 136 * Finish encoding attachment names for Exchange 2003. 137 * @param str A partially encoded string. 138 * @return The fully encoded version of str. 139 */ 140 private static String encodeForExchange2003(final String str) { 141 final AttachmentNameEncoder enc = new AttachmentNameEncoder(); 142 final StringBuilder sb = new StringBuilder(str.length() + 16); 143 enc.appendPartiallyEncoded(sb, str); 144 return sb.toString(); 145 } 146 147 /** 148 * Make the appropriate Exchange server request for getting the attachment. 149 * @param attachment The {@link Attachment} we wish to load. 150 * @return The {@link EasResponse} for the request, or null if we encountered an error. 151 */ 152 private EasResponse performServerRequest(final Attachment attachment) { 153 try { 154 // The method of attachment loading is different in EAS 14.0 than in earlier versions 155 final String cmd; 156 final byte[] bytes; 157 if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { 158 final Serializer s = new Serializer(); 159 s.start(Tags.ITEMS_ITEMS).start(Tags.ITEMS_FETCH); 160 s.data(Tags.ITEMS_STORE, "Mailbox"); 161 s.data(Tags.BASE_FILE_REFERENCE, attachment.mLocation); 162 s.end().end().done(); // ITEMS_FETCH, ITEMS_ITEMS 163 cmd = "ItemOperations"; 164 bytes = s.toByteArray(); 165 } else { 166 final String location; 167 // For Exchange 2003 (EAS 2.5), we have to look for illegal chars in the file name 168 // that EAS sent to us! 169 if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 170 location = encodeForExchange2003(attachment.mLocation); 171 } else { 172 location = attachment.mLocation; 173 } 174 cmd = "GetAttachment&AttachmentName=" + location; 175 bytes = null; 176 } 177 return sendHttpClientPost(cmd, bytes); 178 } catch (final IOException e) { 179 LogUtils.w(TAG, "IOException while loading attachment from server: %s", e.getMessage()); 180 return null; 181 } catch (final CertificateException e) { 182 LogUtils.w(TAG, "CertificateException while loading attachment from server: %s", 183 e.getMessage()); 184 return null; 185 } 186 } 187 188 /** 189 * Close, ignoring errors (as during cleanup) 190 * @param c a Closeable 191 */ 192 private static void close(final Closeable c) { 193 try { 194 c.close(); 195 } catch (IOException e) { 196 LogUtils.w(TAG, "IOException while cleaning up attachment: %s", e.getMessage()); 197 } 198 } 199 200 /** 201 * Save away the contentUri for this Attachment and notify listeners 202 */ 203 private boolean finishLoadAttachment(final Attachment attachment, final File file) { 204 final InputStream in; 205 try { 206 in = new FileInputStream(file); 207 } catch (final FileNotFoundException e) { 208 // Unlikely, as we just created it successfully, but log it. 209 LogUtils.e(TAG, "Could not open attachment file: %s", e.getMessage()); 210 return false; 211 } 212 AttachmentUtilities.saveAttachment(mContext, in, attachment); 213 close(in); 214 return true; 215 } 216 217 /** 218 * Read the {@link EasResponse} and extract the attachment data, saving it to the provider. 219 * @param resp The (successful) {@link EasResponse} containing the attachment data. 220 * @param attachment The {@link Attachment} with the attachment metadata. 221 * @return A status code, from {@link EmailServiceStatus}, for this load. 222 */ 223 private int handleResponse(final EasResponse resp, final Attachment attachment) { 224 final File tmpFile; 225 try { 226 tmpFile = File.createTempFile("eas_", "tmp", mContext.getCacheDir()); 227 } catch (final IOException e) { 228 LogUtils.w(TAG, "Could not open temp file: %s", e.getMessage()); 229 // TODO: This is what the old implementation did, but it's kind of the wrong error. 230 return EmailServiceStatus.CONNECTION_ERROR; 231 } 232 233 try { 234 final OutputStream os; 235 try { 236 os = new FileOutputStream(tmpFile); 237 } catch (final FileNotFoundException e) { 238 LogUtils.w(TAG, "Temp file not found: %s", e.getMessage()); 239 return EmailServiceStatus.ATTACHMENT_NOT_FOUND; 240 } 241 try { 242 final InputStream is = resp.getInputStream(); 243 try { 244 final ProgressCallback callback = new ProgressCallback(mCallback, attachment); 245 final boolean success; 246 if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { 247 final ItemOperationsParser parser = new ItemOperationsParser(is, os, 248 attachment.mSize, callback); 249 parser.parse(); 250 success = (parser.getStatusCode() == 1); 251 } else { 252 final int length = resp.getLength(); 253 if (length != 0) { 254 // len > 0 means that Content-Length was set in the headers 255 // len < 0 means "chunked" transfer-encoding 256 ItemOperationsParser.readChunked(is, os, 257 (length < 0) ? attachment.mSize : length, callback); 258 } 259 success = true; 260 } 261 final int status; 262 if (success && finishLoadAttachment(attachment, tmpFile)) { 263 status = EmailServiceStatus.SUCCESS; 264 } else { 265 status = EmailServiceStatus.CONNECTION_ERROR; 266 } 267 return status; 268 } catch (final IOException e) { 269 LogUtils.w(TAG, "Error reading attachment: %s", e.getMessage()); 270 return EmailServiceStatus.CONNECTION_ERROR; 271 } finally { 272 close(is); 273 } 274 } finally { 275 close(os); 276 } 277 } finally { 278 tmpFile.delete(); 279 } 280 } 281 282 /** 283 * Load the attachment from the server. 284 * @param attachment The attachment to load. 285 * @return A status code, from {@link EmailServiceStatus}, for this load. 286 */ 287 private int load(final Attachment attachment) { 288 // Send a progress update that we're starting. 289 doStatusCallback(mCallback, attachment.mMessageKey, attachment.mId, 290 EmailServiceStatus.IN_PROGRESS, 0); 291 final EasResponse resp = performServerRequest(attachment); 292 if (resp == null) { 293 return EmailServiceStatus.CONNECTION_ERROR; 294 } 295 296 try { 297 if (resp.getStatus() != HttpStatus.SC_OK || resp.isEmpty()) { 298 return EmailServiceStatus.ATTACHMENT_NOT_FOUND; 299 } 300 return handleResponse(resp, attachment); 301 } finally { 302 resp.close(); 303 } 304 } 305 306 } 307