Home | History | Annotate | Download | only in service
      1 package com.android.exchange.service;
      2 
      3 import android.content.Context;
      4 import android.os.RemoteException;
      5 
      6 import com.android.emailcommon.provider.Account;
      7 import com.android.emailcommon.provider.EmailContent.Message;
      8 import com.android.emailcommon.provider.EmailContent.Attachment;
      9 import com.android.emailcommon.service.EmailServiceStatus;
     10 import com.android.emailcommon.service.IEmailServiceCallback;
     11 import com.android.emailcommon.utility.AttachmentUtilities;
     12 import com.android.exchange.Eas;
     13 import com.android.exchange.EasResponse;
     14 import com.android.exchange.adapter.ItemOperationsParser;
     15 import com.android.exchange.adapter.Serializer;
     16 import com.android.exchange.adapter.Tags;
     17 import com.android.exchange.utility.UriCodec;
     18 import com.android.mail.utils.LogUtils;
     19 
     20 import org.apache.http.HttpStatus;
     21 
     22 import java.io.Closeable;
     23 import java.io.File;
     24 import java.io.FileInputStream;
     25 import java.io.FileNotFoundException;
     26 import java.io.FileOutputStream;
     27 import java.io.IOException;
     28 import java.io.InputStream;
     29 import java.io.OutputStream;
     30 import java.security.cert.CertificateException;
     31 
     32 /**
     33  * Loads attachments from the Exchange server.
     34  * TODO: Add ability to call back to UI when this failed, and generally better handle error cases.
     35  */
     36 public class EasAttachmentLoader extends EasServerConnection {
     37     private static final String TAG = Eas.LOG_TAG;
     38 
     39     private final IEmailServiceCallback mCallback;
     40 
     41     private EasAttachmentLoader(final Context context, final Account account,
     42             final IEmailServiceCallback callback) {
     43         super(context, account);
     44         mCallback = callback;
     45     }
     46 
     47     // TODO: EmailServiceStatus.ATTACHMENT_NOT_FOUND is heavily used, may need to split that into
     48     // different statuses.
     49     private static void doStatusCallback(final IEmailServiceCallback callback,
     50             final long messageKey, final long attachmentId, final int status, final int progress) {
     51         if (callback != null) {
     52             try {
     53                 callback.loadAttachmentStatus(messageKey, attachmentId, status, progress);
     54             } catch (final RemoteException e) {
     55                 LogUtils.e(TAG, "RemoteException in loadAttachment: %s", e.getMessage());
     56             }
     57         }
     58     }
     59 
     60     /**
     61      * Provides the parser with the data it needs to perform the callback.
     62      */
     63     public static class ProgressCallback {
     64         private final IEmailServiceCallback mCallback;
     65         private final Attachment mAttachment;
     66 
     67         public ProgressCallback(final IEmailServiceCallback callback,
     68                 final Attachment attachment) {
     69             mCallback = callback;
     70             mAttachment = attachment;
     71         }
     72 
     73         public void doCallback(final int progress) {
     74             doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachment.mId,
     75                     EmailServiceStatus.IN_PROGRESS, progress);
     76         }
     77     }
     78 
     79     /**
     80      * Load an attachment from the Exchange server, and write it to the content provider.
     81      * @param context Our {@link Context}.
     82      * @param attachmentId The local id of the attachment (i.e. its id in the database).
     83      * @param callback The callback for any status updates.
     84      */
     85     public static void loadAttachment(final Context context, final long attachmentId,
     86             final IEmailServiceCallback callback) {
     87         final Attachment attachment = Attachment.restoreAttachmentWithId(context, attachmentId);
     88         if (attachment == null) {
     89             LogUtils.d(TAG, "Could not load attachment %d", attachmentId);
     90             doStatusCallback(callback, -1, attachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND,
     91                     0);
     92             return;
     93         }
     94         if (attachment.mLocation == null) {
     95             LogUtils.e(TAG, "Attachment %d lacks a location", attachmentId);
     96             doStatusCallback(callback, -1, attachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND,
     97                     0);
     98             return;
     99         }
    100         final Account account = Account.restoreAccountWithId(context, attachment.mAccountKey);
    101         if (account == null) {
    102             LogUtils.d(TAG, "Attachment %d has bad account key %d", attachment.mId,
    103                     attachment.mAccountKey);
    104             doStatusCallback(callback, attachment.mMessageKey, attachmentId,
    105                     EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0);
    106             return;
    107         }
    108         final Message message = Message.restoreMessageWithId(context, attachment.mMessageKey);
    109         if (message == null) {
    110             doStatusCallback(callback, attachment.mMessageKey, attachmentId,
    111                 EmailServiceStatus.MESSAGE_NOT_FOUND, 0);
    112             return;
    113         }
    114 
    115         // Error cases handled, do the load.
    116         final EasAttachmentLoader loader =
    117                 new EasAttachmentLoader(context, account, callback);
    118         final int status = loader.load(attachment);
    119         doStatusCallback(callback, attachment.mMessageKey, attachmentId, status, 0);
    120     }
    121 
    122     /**
    123      * Encoder for Exchange 2003 attachment names.  They come from the server partially encoded,
    124      * but there are still possible characters that need to be encoded (Why, MSFT, why?)
    125      */
    126     private static class AttachmentNameEncoder extends UriCodec {
    127         @Override
    128         protected boolean isRetained(final char c) {
    129             // These four characters are commonly received in EAS 2.5 attachment names and are
    130             // valid (verified by testing); we won't encode them
    131             return c == '_' || c == ':' || c == '/' || c == '.';
    132         }
    133     }
    134 
    135     /**
    136      * Finish encoding attachment names for Exchange 2003.
    137      * @param str A partially encoded string.
    138      * @return The fully encoded version of str.
    139      */
    140     private static String encodeForExchange2003(final String str) {
    141         final AttachmentNameEncoder enc = new AttachmentNameEncoder();
    142         final StringBuilder sb = new StringBuilder(str.length() + 16);
    143         enc.appendPartiallyEncoded(sb, str);
    144         return sb.toString();
    145     }
    146 
    147     /**
    148      * Make the appropriate Exchange server request for getting the attachment.
    149      * @param attachment The {@link Attachment} we wish to load.
    150      * @return The {@link EasResponse} for the request, or null if we encountered an error.
    151      */
    152     private EasResponse performServerRequest(final Attachment attachment) {
    153         try {
    154             // The method of attachment loading is different in EAS 14.0 than in earlier versions
    155             final String cmd;
    156             final byte[] bytes;
    157             if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
    158                 final Serializer s = new Serializer();
    159                 s.start(Tags.ITEMS_ITEMS).start(Tags.ITEMS_FETCH);
    160                 s.data(Tags.ITEMS_STORE, "Mailbox");
    161                 s.data(Tags.BASE_FILE_REFERENCE, attachment.mLocation);
    162                 s.end().end().done(); // ITEMS_FETCH, ITEMS_ITEMS
    163                 cmd = "ItemOperations";
    164                 bytes = s.toByteArray();
    165             } else {
    166                 final String location;
    167                 // For Exchange 2003 (EAS 2.5), we have to look for illegal chars in the file name
    168                 // that EAS sent to us!
    169                 if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
    170                     location = encodeForExchange2003(attachment.mLocation);
    171                 } else {
    172                     location = attachment.mLocation;
    173                 }
    174                 cmd = "GetAttachment&AttachmentName=" + location;
    175                 bytes = null;
    176             }
    177             return sendHttpClientPost(cmd, bytes);
    178         } catch (final IOException e) {
    179             LogUtils.w(TAG, "IOException while loading attachment from server: %s", e.getMessage());
    180             return null;
    181         } catch (final CertificateException e) {
    182             LogUtils.w(TAG, "CertificateException while loading attachment from server: %s",
    183                     e.getMessage());
    184             return null;
    185         }
    186     }
    187 
    188     /**
    189      * Close, ignoring errors (as during cleanup)
    190      * @param c a Closeable
    191      */
    192     private static void close(final Closeable c) {
    193         try {
    194             c.close();
    195         } catch (IOException e) {
    196             LogUtils.w(TAG, "IOException while cleaning up attachment: %s", e.getMessage());
    197         }
    198     }
    199 
    200     /**
    201      * Save away the contentUri for this Attachment and notify listeners
    202      */
    203     private boolean finishLoadAttachment(final Attachment attachment, final File file) {
    204         final InputStream in;
    205         try {
    206             in = new FileInputStream(file);
    207           } catch (final FileNotFoundException e) {
    208             // Unlikely, as we just created it successfully, but log it.
    209             LogUtils.e(TAG, "Could not open attachment file: %s", e.getMessage());
    210             return false;
    211         }
    212         AttachmentUtilities.saveAttachment(mContext, in, attachment);
    213         close(in);
    214         return true;
    215     }
    216 
    217     /**
    218      * Read the {@link EasResponse} and extract the attachment data, saving it to the provider.
    219      * @param resp The (successful) {@link EasResponse} containing the attachment data.
    220      * @param attachment The {@link Attachment} with the attachment metadata.
    221      * @return A status code, from {@link EmailServiceStatus}, for this load.
    222      */
    223     private int handleResponse(final EasResponse resp, final Attachment attachment) {
    224         final File tmpFile;
    225         try {
    226             tmpFile = File.createTempFile("eas_", "tmp", mContext.getCacheDir());
    227         } catch (final IOException e) {
    228             LogUtils.w(TAG, "Could not open temp file: %s", e.getMessage());
    229             // TODO: This is what the old implementation did, but it's kind of the wrong error.
    230             return EmailServiceStatus.CONNECTION_ERROR;
    231         }
    232 
    233         try {
    234             final OutputStream os;
    235             try {
    236                 os = new FileOutputStream(tmpFile);
    237             } catch (final FileNotFoundException e) {
    238                 LogUtils.w(TAG, "Temp file not found: %s", e.getMessage());
    239                 return EmailServiceStatus.ATTACHMENT_NOT_FOUND;
    240             }
    241             try {
    242                 final InputStream is = resp.getInputStream();
    243                 try {
    244                     final ProgressCallback callback = new ProgressCallback(mCallback, attachment);
    245                     final boolean success;
    246                     if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
    247                         final ItemOperationsParser parser = new ItemOperationsParser(is, os,
    248                                 attachment.mSize, callback);
    249                         parser.parse();
    250                         success = (parser.getStatusCode() == 1);
    251                     } else {
    252                         final int length = resp.getLength();
    253                         if (length != 0) {
    254                             // len > 0 means that Content-Length was set in the headers
    255                             // len < 0 means "chunked" transfer-encoding
    256                             ItemOperationsParser.readChunked(is, os,
    257                                     (length < 0) ? attachment.mSize : length, callback);
    258                         }
    259                         success = true;
    260                     }
    261                     final int status;
    262                     if (success && finishLoadAttachment(attachment, tmpFile)) {
    263                         status = EmailServiceStatus.SUCCESS;
    264                     } else {
    265                         status = EmailServiceStatus.CONNECTION_ERROR;
    266                     }
    267                     return status;
    268                 } catch (final IOException e) {
    269                     LogUtils.w(TAG, "Error reading attachment: %s", e.getMessage());
    270                     return EmailServiceStatus.CONNECTION_ERROR;
    271                 } finally {
    272                     close(is);
    273                 }
    274             } finally {
    275                 close(os);
    276             }
    277         } finally {
    278             tmpFile.delete();
    279         }
    280     }
    281 
    282     /**
    283      * Load the attachment from the server.
    284      * @param attachment The attachment to load.
    285      * @return A status code, from {@link EmailServiceStatus}, for this load.
    286      */
    287     private int load(final Attachment attachment) {
    288         // Send a progress update that we're starting.
    289         doStatusCallback(mCallback, attachment.mMessageKey, attachment.mId,
    290                 EmailServiceStatus.IN_PROGRESS, 0);
    291         final EasResponse resp = performServerRequest(attachment);
    292         if (resp == null) {
    293             return EmailServiceStatus.CONNECTION_ERROR;
    294         }
    295 
    296         try {
    297             if (resp.getStatus() != HttpStatus.SC_OK || resp.isEmpty()) {
    298                 return EmailServiceStatus.ATTACHMENT_NOT_FOUND;
    299             }
    300             return handleResponse(resp, attachment);
    301         } finally {
    302             resp.close();
    303         }
    304     }
    305 
    306 }
    307