Home | History | Annotate | Download | only in eas
      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