• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
AttachmentLoader(EasSyncService service, PartRequest req)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 
doStatusCallback(int status)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 
doProgressCallback(int progress)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      */
finishLoadAttachment()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      */
readChunked(InputStream inputStream, OutputStream outputStream, int len)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
encodeForExchange2003(String str)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 {
isRetained(char c)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      */
loadAttachment()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