Home | History | Annotate | Download | only in adapter
      1 /* Copyright (C) 2011 The Android Open Source Project.
      2  *
      3  * Licensed under the Apache License, Version 2.0 (the "License");
      4  * you may not use this file except in compliance with the License.
      5  * You may obtain a copy of the License at
      6  *
      7  *      http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software
     10  * distributed under the License is distributed on an "AS IS" BASIS,
     11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12  * See the License for the specific language governing permissions and
     13  * limitations under the License.
     14  */
     15 
     16 package com.android.exchange.adapter;
     17 
     18 import android.content.ContentResolver;
     19 import android.content.ContentValues;
     20 import android.content.Context;
     21 import android.net.Uri;
     22 import android.os.RemoteException;
     23 
     24 import com.android.emailcommon.provider.EmailContent.Attachment;
     25 import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
     26 import com.android.emailcommon.provider.EmailContent.Message;
     27 import com.android.emailcommon.service.EmailServiceStatus;
     28 import com.android.emailcommon.utility.AttachmentUtilities;
     29 import com.android.exchange.Eas;
     30 import com.android.exchange.EasResponse;
     31 import com.android.exchange.EasSyncService;
     32 import com.android.exchange.ExchangeService;
     33 import com.android.exchange.PartRequest;
     34 import com.android.exchange.utility.UriCodec;
     35 import com.android.mail.providers.UIProvider;
     36 import com.google.common.annotations.VisibleForTesting;
     37 
     38 import org.apache.http.HttpStatus;
     39 
     40 import java.io.FileNotFoundException;
     41 import java.io.IOException;
     42 import java.io.InputStream;
     43 import java.io.OutputStream;
     44 
     45 /**
     46  * Handle EAS attachment loading, regardless of protocol version
     47  */
     48 public class AttachmentLoader {
     49     static private final int CHUNK_SIZE = 16*1024;
     50 
     51     private final EasSyncService mService;
     52     private final Context mContext;
     53     private final ContentResolver mResolver;
     54     private final Attachment mAttachment;
     55     private final long mAttachmentId;
     56     private final int mAttachmentSize;
     57     private final long mMessageId;
     58     private final Message mMessage;
     59     private final long mAccountId;
     60     private final Uri mAttachmentUri;
     61 
     62     public AttachmentLoader(EasSyncService service, PartRequest req) {
     63         mService = service;
     64         mContext = service.mContext;
     65         mResolver = service.mContentResolver;
     66         mAttachment = req.mAttachment;
     67         mAttachmentId = mAttachment.mId;
     68         mAttachmentSize = (int)mAttachment.mSize;
     69         mAccountId = mAttachment.mAccountKey;
     70         mMessageId = mAttachment.mMessageKey;
     71         mMessage = Message.restoreMessageWithId(mContext, mMessageId);
     72         mAttachmentUri = AttachmentUtilities.getAttachmentUri(mAccountId, mAttachmentId);
     73     }
     74 
     75     private void doStatusCallback(int status) {
     76         try {
     77             ExchangeService.callback().loadAttachmentStatus(mMessageId, mAttachmentId, status, 0);
     78         } catch (RemoteException e) {
     79             // No danger if the client is no longer around
     80         }
     81     }
     82 
     83     private void doProgressCallback(int progress) {
     84         try {
     85             ExchangeService.callback().loadAttachmentStatus(mMessageId, mAttachmentId,
     86                     EmailServiceStatus.IN_PROGRESS, progress);
     87         } catch (RemoteException e) {
     88             // No danger if the client is no longer around
     89         }
     90     }
     91 
     92     /**
     93      * Save away the contentUri for this Attachment and notify listeners
     94      */
     95     private void finishLoadAttachment() {
     96         ContentValues cv = new ContentValues();
     97         cv.put(AttachmentColumns.CONTENT_URI, mAttachmentUri.toString());
     98         cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED);
     99         mAttachment.update(mContext, cv);
    100         doStatusCallback(EmailServiceStatus.SUCCESS);
    101     }
    102 
    103     /**
    104      * Read the attachment data in chunks and write the data back out to our attachment file
    105      * @param inputStream the InputStream we're reading the attachment from
    106      * @param outputStream the OutputStream the attachment will be written to
    107      * @param len the number of expected bytes we're going to read
    108      * @throws IOException
    109      */
    110     public void readChunked(InputStream inputStream, OutputStream outputStream, int len)
    111             throws IOException {
    112         byte[] bytes = new byte[CHUNK_SIZE];
    113         int length = len;
    114         // Loop terminates 1) when EOF is reached or 2) IOException occurs
    115         // One of these is guaranteed to occur
    116         int totalRead = 0;
    117         int lastCallbackPct = -1;
    118         int lastCallbackTotalRead = 0;
    119         mService.userLog("Expected attachment length: ", len);
    120         while (true) {
    121             int read = inputStream.read(bytes, 0, CHUNK_SIZE);
    122             if (read < 0) {
    123                 // -1 means EOF
    124                 mService.userLog("Attachment load reached EOF, totalRead: ", totalRead);
    125                 break;
    126             }
    127 
    128             // Keep track of how much we've read for progress callback
    129             totalRead += read;
    130             // Write these bytes out
    131             outputStream.write(bytes, 0, read);
    132 
    133             // We can't report percentage if data is chunked; the length of incoming data is unknown
    134             if (length > 0) {
    135                 int pct = (totalRead * 100) / length;
    136                 // Callback only if we've read at least 1% more and have read more than CHUNK_SIZE
    137                 // We don't want to spam the Email app
    138                 if ((pct > lastCallbackPct) && (totalRead > (lastCallbackTotalRead + CHUNK_SIZE))) {
    139                     // Report progress back to the UI
    140                     doProgressCallback(pct);
    141                     lastCallbackTotalRead = totalRead;
    142                     lastCallbackPct = pct;
    143                 }
    144             }
    145         }
    146         if (totalRead > length) {
    147             // Apparently, the length, as reported by EAS, isn't always accurate; let's log it
    148             mService.userLog("Read more than expected: ", totalRead);
    149         }
    150     }
    151 
    152     @VisibleForTesting
    153     static String encodeForExchange2003(String str) {
    154         AttachmentNameEncoder enc = new AttachmentNameEncoder();
    155         StringBuilder sb = new StringBuilder(str.length() + 16);
    156         enc.appendPartiallyEncoded(sb, str);
    157         return sb.toString();
    158     }
    159 
    160     /**
    161      * Encoder for Exchange 2003 attachment names.  They come from the server partially encoded,
    162      * but there are still possible characters that need to be encoded (Why, MSFT, why?)
    163      */
    164     private static class AttachmentNameEncoder extends UriCodec {
    165         @Override protected boolean isRetained(char c) {
    166             // These four characters are commonly received in EAS 2.5 attachment names and are
    167             // valid (verified by testing); we won't encode them
    168             return c == '_' || c == ':' || c == '/' || c == '.';
    169         }
    170     }
    171 
    172     /**
    173      * Loads an attachment, based on the PartRequest passed in the constructor
    174      * @throws IOException
    175      */
    176     public void loadAttachment() throws IOException {
    177         if (mMessage == null) {
    178             doStatusCallback(EmailServiceStatus.MESSAGE_NOT_FOUND);
    179             return;
    180         }
    181         // Say we've started loading the attachment
    182         doProgressCallback(0);
    183 
    184         EasResponse resp;
    185         boolean eas14 = mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE;
    186         // The method of attachment loading is different in EAS 14.0 than in earlier versions
    187         if (eas14) {
    188             Serializer s = new Serializer();
    189             s.start(Tags.ITEMS_ITEMS).start(Tags.ITEMS_FETCH);
    190             s.data(Tags.ITEMS_STORE, "Mailbox");
    191             s.data(Tags.BASE_FILE_REFERENCE, mAttachment.mLocation);
    192             s.end().end().done(); // ITEMS_FETCH, ITEMS_ITEMS
    193             resp = mService.sendHttpClientPost("ItemOperations", s.toByteArray());
    194         } else {
    195             String location = mAttachment.mLocation;
    196             // For Exchange 2003 (EAS 2.5), we have to look for illegal characters in the file name
    197             // that EAS sent to us!
    198             if (mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
    199                 location = encodeForExchange2003(location);
    200             }
    201             String cmd = "GetAttachment&AttachmentName=" + location;
    202             resp = mService.sendHttpClientPost(cmd, null, EasSyncService.COMMAND_TIMEOUT);
    203         }
    204 
    205         try {
    206             int status = resp.getStatus();
    207             if (status == HttpStatus.SC_OK) {
    208                 if (!resp.isEmpty()) {
    209                     InputStream is = resp.getInputStream();
    210                     OutputStream os = null;
    211                     try {
    212                         os = mResolver.openOutputStream(mAttachmentUri);
    213                         if (eas14) {
    214                             ItemOperationsParser p = new ItemOperationsParser(this, is, os,
    215                                     mAttachmentSize);
    216                             p.parse();
    217                             if (p.getStatusCode() == 1 /* Success */) {
    218                                 finishLoadAttachment();
    219                                 return;
    220                             }
    221                         } else {
    222                             int len = resp.getLength();
    223                             if (len != 0) {
    224                                 // len > 0 means that Content-Length was set in the headers
    225                                 // len < 0 means "chunked" transfer-encoding
    226                                 readChunked(is, os, (len < 0) ? mAttachmentSize : len);
    227                                 finishLoadAttachment();
    228                                 return;
    229                             }
    230                         }
    231                     } catch (FileNotFoundException e) {
    232                         mService.errorLog("Can't get attachment; write file not found?");
    233                     } finally {
    234                         if (os != null) {
    235                             os.flush();
    236                             os.close();
    237                         }
    238                     }
    239                 }
    240             }
    241         } finally {
    242             resp.close();
    243         }
    244 
    245         // All errors lead here...
    246         doStatusCallback(EmailServiceStatus.ATTACHMENT_NOT_FOUND);
    247     }
    248 }
    249