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 */ 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 */ 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 107 public ProgressCallback(final IEmailServiceCallback callback, 108 final EmailContent.Attachment attachment) { 109 mCallback = callback; 110 mAttachment = attachment; 111 } 112 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 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 */ 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 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 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 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 */ 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 */ 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 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_REQUEST_FAILURE; 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_REQUEST_FAILURE; 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