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