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