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