• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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