1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.exchange.eas; 18 19 import android.content.Context; 20 import android.os.RemoteException; 21 22 import com.android.emailcommon.provider.EmailContent; 23 import com.android.emailcommon.provider.EmailContent.Attachment; 24 import com.android.emailcommon.service.EmailServiceStatus; 25 import com.android.emailcommon.service.IEmailServiceCallback; 26 import com.android.emailcommon.utility.AttachmentUtilities; 27 import com.android.exchange.Eas; 28 import com.android.exchange.EasResponse; 29 import com.android.exchange.adapter.ItemOperationsParser; 30 import com.android.exchange.adapter.Serializer; 31 import com.android.exchange.adapter.Tags; 32 import com.android.exchange.service.EasService; 33 import com.android.exchange.utility.UriCodec; 34 import com.android.mail.utils.LogUtils; 35 36 import org.apache.http.HttpEntity; 37 38 import java.io.Closeable; 39 import java.io.File; 40 import java.io.FileInputStream; 41 import java.io.FileNotFoundException; 42 import java.io.FileOutputStream; 43 import java.io.IOException; 44 import java.io.InputStream; 45 import java.io.OutputStream; 46 47 /** 48 * This class performs the heavy lifting of loading attachments from the Exchange server to the 49 * device in a local file. 50 * TODO: Add ability to call back to UI when this failed, and generally better handle error cases. 51 */ 52 public final class EasLoadAttachment extends EasOperation { 53 54 public static final int RESULT_SUCCESS = 0; 55 56 /** Attachment Loading Errors **/ 57 public static final int RESULT_LOAD_ATTACHMENT_INFO_ERROR = -100; 58 public static final int RESULT_ATTACHMENT_NO_LOCATION_ERROR = -101; 59 public static final int RESULT_ATTACHMENT_LOAD_MESSAGE_ERROR = -102; 60 public static final int RESULT_ATTACHMENT_INTERNAL_HANDLING_ERROR = -103; 61 public static final int RESULT_ATTACHMENT_RESPONSE_PARSING_ERROR = -104; 62 63 private final IEmailServiceCallback mCallback; 64 private final long mAttachmentId; 65 66 // These members are set in a future point in time outside of the constructor. 67 private Attachment mAttachment; 68 69 /** 70 * Constructor for use with {@link EasService} when performing an actual sync. 71 * @param context Our {@link Context}. 72 * @param accountId The id of the account in question (i.e. its id in the database). 73 * @param attachmentId The local id of the attachment (i.e. its id in the database). 74 * @param callback The callback for any status updates. 75 */ EasLoadAttachment(final Context context, final long accountId, final long attachmentId, final IEmailServiceCallback callback)76 public EasLoadAttachment(final Context context, final long accountId, final long attachmentId, 77 final IEmailServiceCallback callback) { 78 // The account is loaded before performOperation but it is not guaranteed to be available 79 // before then. 80 super(context, accountId); 81 mCallback = callback; 82 mAttachmentId = attachmentId; 83 } 84 85 /** 86 * Helper function that makes a callback for us within our implementation. 87 */ doStatusCallback(final IEmailServiceCallback callback, final long messageKey, final long attachmentId, final int status, final int progress)88 private static void doStatusCallback(final IEmailServiceCallback callback, 89 final long messageKey, final long attachmentId, final int status, final int progress) { 90 if (callback != null) { 91 try { 92 // loadAttachmentStatus is mart of IEmailService interface. 93 callback.loadAttachmentStatus(messageKey, attachmentId, status, progress); 94 } catch (final RemoteException e) { 95 LogUtils.e(LOG_TAG, "RemoteException in loadAttachment: %s", e.getMessage()); 96 } 97 } 98 } 99 100 /** 101 * Helper class that is passed to other objects to perform callbacks for us. 102 */ 103 public static class ProgressCallback { 104 private final IEmailServiceCallback mCallback; 105 private final EmailContent.Attachment mAttachment; 106 ProgressCallback(final IEmailServiceCallback callback, final EmailContent.Attachment attachment)107 public ProgressCallback(final IEmailServiceCallback callback, 108 final EmailContent.Attachment attachment) { 109 mCallback = callback; 110 mAttachment = attachment; 111 } 112 doCallback(final int progress)113 public void doCallback(final int progress) { 114 doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachment.mId, 115 EmailServiceStatus.IN_PROGRESS, progress); 116 } 117 } 118 119 /** 120 * Encoder for Exchange 2003 attachment names. They come from the server partially encoded, 121 * but there are still possible characters that need to be encoded (Why, MSFT, why?) 122 */ 123 private static class AttachmentNameEncoder extends UriCodec { 124 @Override isRetained(final char c)125 protected boolean isRetained(final char c) { 126 // These four characters are commonly received in EAS 2.5 attachment names and are 127 // valid (verified by testing); we won't encode them 128 return c == '_' || c == ':' || c == '/' || c == '.'; 129 } 130 } 131 132 /** 133 * Finish encoding attachment names for Exchange 2003. 134 * @param str A partially encoded string. 135 * @return The fully encoded version of str. 136 */ encodeForExchange2003(final String str)137 private static String encodeForExchange2003(final String str) { 138 final AttachmentNameEncoder enc = new AttachmentNameEncoder(); 139 final StringBuilder sb = new StringBuilder(str.length() + 16); 140 enc.appendPartiallyEncoded(sb, str); 141 return sb.toString(); 142 } 143 144 /** 145 * Finish encoding attachment names for Exchange 2003. 146 * @return A {@link EmailServiceStatus} code that indicates the result of the operation. 147 */ 148 @Override performOperation()149 public int performOperation() { 150 mAttachment = EmailContent.Attachment.restoreAttachmentWithId(mContext, mAttachmentId); 151 if (mAttachment == null) { 152 LogUtils.e(LOG_TAG, "Could not load attachment %d", mAttachmentId); 153 doStatusCallback(mCallback, -1, mAttachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND, 154 0); 155 return RESULT_LOAD_ATTACHMENT_INFO_ERROR; 156 } 157 if (mAttachment.mLocation == null) { 158 LogUtils.e(LOG_TAG, "Attachment %d lacks a location", mAttachmentId); 159 doStatusCallback(mCallback, -1, mAttachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND, 160 0); 161 return RESULT_ATTACHMENT_NO_LOCATION_ERROR; 162 } 163 final EmailContent.Message message = EmailContent.Message 164 .restoreMessageWithId(mContext, mAttachment.mMessageKey); 165 if (message == null) { 166 LogUtils.e(LOG_TAG, "Could not load message %d", mAttachment.mMessageKey); 167 doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachmentId, 168 EmailServiceStatus.MESSAGE_NOT_FOUND, 0); 169 return RESULT_ATTACHMENT_LOAD_MESSAGE_ERROR; 170 } 171 172 // First callback to let the client know that we have started the attachment load. 173 doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachmentId, 174 EmailServiceStatus.IN_PROGRESS, 0); 175 176 final int result = super.performOperation(); 177 178 // Last callback to report results. 179 if (result < 0) { 180 // We had an error processing an attachment, let's report a {@link EmailServiceStatus} 181 // connection error in this case 182 LogUtils.d(LOG_TAG, "Invoking callback for attachmentId: %d with CONNECTION_ERROR", 183 mAttachmentId); 184 doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachmentId, 185 EmailServiceStatus.CONNECTION_ERROR, 0); 186 } else { 187 LogUtils.d(LOG_TAG, "Invoking callback for attachmentId: %d with SUCCESS", 188 mAttachmentId); 189 doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachmentId, 190 EmailServiceStatus.SUCCESS, 0); 191 } 192 return result; 193 } 194 195 @Override getCommand()196 protected String getCommand() { 197 if (mAttachment == null) { 198 LogUtils.wtf(LOG_TAG, "Error, mAttachment is null"); 199 } 200 201 final String cmd; 202 if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { 203 // The operation is different in EAS 14.0 than in earlier versions 204 cmd = "ItemOperations"; 205 } else { 206 final String location; 207 // For Exchange 2003 (EAS 2.5), we have to look for illegal chars in the file name 208 // that EAS sent to us! 209 if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 210 location = encodeForExchange2003(mAttachment.mLocation); 211 } else { 212 location = mAttachment.mLocation; 213 } 214 cmd = "GetAttachment&AttachmentName=" + location; 215 } 216 return cmd; 217 } 218 219 @Override getRequestEntity()220 protected HttpEntity getRequestEntity() throws IOException { 221 if (mAttachment == null) { 222 LogUtils.wtf(LOG_TAG, "Error, mAttachment is null"); 223 } 224 225 final HttpEntity entity; 226 final Serializer s = new Serializer(); 227 if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { 228 s.start(Tags.ITEMS_ITEMS).start(Tags.ITEMS_FETCH); 229 s.data(Tags.ITEMS_STORE, "Mailbox"); 230 s.data(Tags.BASE_FILE_REFERENCE, mAttachment.mLocation); 231 s.end().end().done(); // ITEMS_FETCH, ITEMS_ITEMS 232 entity = makeEntity(s); 233 } else { 234 // Older versions of the protocol have the attachment location in the command. 235 entity = null; 236 } 237 return entity; 238 } 239 240 /** 241 * Close, ignoring errors (as during cleanup) 242 * @param c a Closeable 243 */ close(final Closeable c)244 private static void close(final Closeable c) { 245 try { 246 c.close(); 247 } catch (IOException e) { 248 LogUtils.e(LOG_TAG, "IOException while cleaning up attachment: %s", e.getMessage()); 249 } 250 } 251 252 /** 253 * Save away the contentUri for this Attachment and notify listeners 254 */ finishLoadAttachment(final EmailContent.Attachment attachment, final File file)255 private boolean finishLoadAttachment(final EmailContent.Attachment attachment, final File file) { 256 final InputStream in; 257 try { 258 in = new FileInputStream(file); 259 } catch (final FileNotFoundException e) { 260 // Unlikely, as we just created it successfully, but log it. 261 LogUtils.e(LOG_TAG, "Could not open attachment file: %s", e.getMessage()); 262 return false; 263 } 264 AttachmentUtilities.saveAttachment(mContext, in, attachment); 265 close(in); 266 return true; 267 } 268 269 /** 270 * Read the {@link EasResponse} and extract the attachment data, saving it to the provider. 271 * @param response The (successful) {@link EasResponse} containing the attachment data. 272 * @return A status code, 0 is a success, anything negative is an error outlined by constants 273 * in this class or its base class. 274 */ 275 @Override handleResponse(final EasResponse response)276 protected int handleResponse(final EasResponse response) { 277 // Some very basic error checking on the response object first. 278 // Our base class should be responsible for checking these errors but if the error 279 // checking is done in the override functions, we can be more specific about 280 // the errors that are being returned to the caller of performOperation(). 281 if (response.isEmpty()) { 282 LogUtils.e(LOG_TAG, "Error, empty response."); 283 return RESULT_NETWORK_PROBLEM; 284 } 285 286 // This is a 2 step process. 287 // 1. Grab what came over the wire and write it to a temp file on disk. 288 // 2. Move the attachment to its final location. 289 final File tmpFile; 290 try { 291 tmpFile = File.createTempFile("eas_", "tmp", mContext.getCacheDir()); 292 } catch (final IOException e) { 293 LogUtils.e(LOG_TAG, "Could not open temp file: %s", e.getMessage()); 294 return RESULT_NETWORK_PROBLEM; 295 } 296 297 try { 298 final OutputStream os; 299 try { 300 os = new FileOutputStream(tmpFile); 301 } catch (final FileNotFoundException e) { 302 LogUtils.e(LOG_TAG, "Temp file not found: %s", e.getMessage()); 303 return RESULT_ATTACHMENT_INTERNAL_HANDLING_ERROR; 304 } 305 try { 306 final InputStream is = response.getInputStream(); 307 try { 308 // TODO: Right now we are explictly loading this from a class 309 // that will be deprecated when we move over to EasService. When we start using 310 // our internal class instead, there will be rippling side effect changes that 311 // need to be made when this time comes. 312 final ProgressCallback callback = new ProgressCallback(mCallback, mAttachment); 313 final boolean success; 314 if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { 315 final ItemOperationsParser parser = new ItemOperationsParser(is, os, 316 mAttachment.mSize, callback); 317 parser.parse(); 318 success = (parser.getStatusCode() == 1); 319 } else { 320 final int length = response.getLength(); 321 if (length != 0) { 322 // len > 0 means that Content-Length was set in the headers 323 // len < 0 means "chunked" transfer-encoding 324 ItemOperationsParser.readChunked(is, os, 325 (length < 0) ? mAttachment.mSize : length, callback); 326 } 327 success = true; 328 } 329 // Check that we successfully grabbed what came over the wire... 330 if (!success) { 331 LogUtils.e(LOG_TAG, "Error parsing server response"); 332 return RESULT_ATTACHMENT_RESPONSE_PARSING_ERROR; 333 } 334 // Now finish the process and save to the final destination. 335 final boolean loadResult = finishLoadAttachment(mAttachment, tmpFile); 336 if (!loadResult) { 337 LogUtils.e(LOG_TAG, "Error post processing attachment file."); 338 return RESULT_ATTACHMENT_INTERNAL_HANDLING_ERROR; 339 } 340 } catch (final IOException e) { 341 LogUtils.e(LOG_TAG, "Error handling attachment: %s", e.getMessage()); 342 return RESULT_ATTACHMENT_INTERNAL_HANDLING_ERROR; 343 } finally { 344 close(is); 345 } 346 } finally { 347 close(os); 348 } 349 } finally { 350 tmpFile.delete(); 351 } 352 return RESULT_SUCCESS; 353 } 354 } 355