| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.exchange.eas; |
| |
| import android.content.Context; |
| import android.os.RemoteException; |
| |
| import com.android.emailcommon.provider.EmailContent; |
| import com.android.emailcommon.provider.EmailContent.Attachment; |
| import com.android.emailcommon.service.EmailServiceStatus; |
| import com.android.emailcommon.service.IEmailServiceCallback; |
| import com.android.emailcommon.utility.AttachmentUtilities; |
| import com.android.exchange.Eas; |
| import com.android.exchange.EasResponse; |
| import com.android.exchange.adapter.ItemOperationsParser; |
| import com.android.exchange.adapter.Serializer; |
| import com.android.exchange.adapter.Tags; |
| import com.android.exchange.service.EasService; |
| import com.android.exchange.utility.UriCodec; |
| import com.android.mail.utils.LogUtils; |
| |
| import org.apache.http.HttpEntity; |
| |
| import java.io.Closeable; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| |
| /** |
| * This class performs the heavy lifting of loading attachments from the Exchange server to the |
| * device in a local file. |
| * TODO: Add ability to call back to UI when this failed, and generally better handle error cases. |
| */ |
| public final class EasLoadAttachment extends EasOperation { |
| |
| public static final int RESULT_SUCCESS = 0; |
| |
| /** Attachment Loading Errors **/ |
| public static final int RESULT_LOAD_ATTACHMENT_INFO_ERROR = -100; |
| public static final int RESULT_ATTACHMENT_NO_LOCATION_ERROR = -101; |
| public static final int RESULT_ATTACHMENT_LOAD_MESSAGE_ERROR = -102; |
| public static final int RESULT_ATTACHMENT_INTERNAL_HANDLING_ERROR = -103; |
| public static final int RESULT_ATTACHMENT_RESPONSE_PARSING_ERROR = -104; |
| |
| private final IEmailServiceCallback mCallback; |
| private final long mAttachmentId; |
| |
| // These members are set in a future point in time outside of the constructor. |
| private Attachment mAttachment; |
| |
| /** |
| * Constructor for use with {@link EasService} when performing an actual sync. |
| * @param context Our {@link Context}. |
| * @param accountId The id of the account in question (i.e. its id in the database). |
| * @param attachmentId The local id of the attachment (i.e. its id in the database). |
| * @param callback The callback for any status updates. |
| */ |
| public EasLoadAttachment(final Context context, final long accountId, final long attachmentId, |
| final IEmailServiceCallback callback) { |
| // The account is loaded before performOperation but it is not guaranteed to be available |
| // before then. |
| super(context, accountId); |
| mCallback = callback; |
| mAttachmentId = attachmentId; |
| } |
| |
| /** |
| * Helper function that makes a callback for us within our implementation. |
| */ |
| private static void doStatusCallback(final IEmailServiceCallback callback, |
| final long messageKey, final long attachmentId, final int status, final int progress) { |
| if (callback != null) { |
| try { |
| // loadAttachmentStatus is mart of IEmailService interface. |
| callback.loadAttachmentStatus(messageKey, attachmentId, status, progress); |
| } catch (final RemoteException e) { |
| LogUtils.e(LOG_TAG, "RemoteException in loadAttachment: %s", e.getMessage()); |
| } |
| } |
| } |
| |
| /** |
| * Helper class that is passed to other objects to perform callbacks for us. |
| */ |
| public static class ProgressCallback { |
| private final IEmailServiceCallback mCallback; |
| private final EmailContent.Attachment mAttachment; |
| |
| public ProgressCallback(final IEmailServiceCallback callback, |
| final EmailContent.Attachment attachment) { |
| mCallback = callback; |
| mAttachment = attachment; |
| } |
| |
| public void doCallback(final int progress) { |
| doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachment.mId, |
| EmailServiceStatus.IN_PROGRESS, progress); |
| } |
| } |
| |
| /** |
| * Encoder for Exchange 2003 attachment names. They come from the server partially encoded, |
| * but there are still possible characters that need to be encoded (Why, MSFT, why?) |
| */ |
| private static class AttachmentNameEncoder extends UriCodec { |
| @Override |
| protected boolean isRetained(final char c) { |
| // These four characters are commonly received in EAS 2.5 attachment names and are |
| // valid (verified by testing); we won't encode them |
| return c == '_' || c == ':' || c == '/' || c == '.'; |
| } |
| } |
| |
| /** |
| * Finish encoding attachment names for Exchange 2003. |
| * @param str A partially encoded string. |
| * @return The fully encoded version of str. |
| */ |
| private static String encodeForExchange2003(final String str) { |
| final AttachmentNameEncoder enc = new AttachmentNameEncoder(); |
| final StringBuilder sb = new StringBuilder(str.length() + 16); |
| enc.appendPartiallyEncoded(sb, str); |
| return sb.toString(); |
| } |
| |
| /** |
| * Finish encoding attachment names for Exchange 2003. |
| * @return A {@link EmailServiceStatus} code that indicates the result of the operation. |
| */ |
| @Override |
| public int performOperation() { |
| mAttachment = EmailContent.Attachment.restoreAttachmentWithId(mContext, mAttachmentId); |
| if (mAttachment == null) { |
| LogUtils.e(LOG_TAG, "Could not load attachment %d", mAttachmentId); |
| doStatusCallback(mCallback, -1, mAttachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND, |
| 0); |
| return RESULT_LOAD_ATTACHMENT_INFO_ERROR; |
| } |
| if (mAttachment.mLocation == null) { |
| LogUtils.e(LOG_TAG, "Attachment %d lacks a location", mAttachmentId); |
| doStatusCallback(mCallback, -1, mAttachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND, |
| 0); |
| return RESULT_ATTACHMENT_NO_LOCATION_ERROR; |
| } |
| final EmailContent.Message message = EmailContent.Message |
| .restoreMessageWithId(mContext, mAttachment.mMessageKey); |
| if (message == null) { |
| LogUtils.e(LOG_TAG, "Could not load message %d", mAttachment.mMessageKey); |
| doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachmentId, |
| EmailServiceStatus.MESSAGE_NOT_FOUND, 0); |
| return RESULT_ATTACHMENT_LOAD_MESSAGE_ERROR; |
| } |
| |
| // First callback to let the client know that we have started the attachment load. |
| doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachmentId, |
| EmailServiceStatus.IN_PROGRESS, 0); |
| |
| final int result = super.performOperation(); |
| |
| // Last callback to report results. |
| if (result < 0) { |
| // We had an error processing an attachment, let's report a {@link EmailServiceStatus} |
| // connection error in this case |
| LogUtils.d(LOG_TAG, "Invoking callback for attachmentId: %d with CONNECTION_ERROR", |
| mAttachmentId); |
| doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachmentId, |
| EmailServiceStatus.CONNECTION_ERROR, 0); |
| } else { |
| LogUtils.d(LOG_TAG, "Invoking callback for attachmentId: %d with SUCCESS", |
| mAttachmentId); |
| doStatusCallback(mCallback, mAttachment.mMessageKey, mAttachmentId, |
| EmailServiceStatus.SUCCESS, 0); |
| } |
| return result; |
| } |
| |
| @Override |
| protected String getCommand() { |
| if (mAttachment == null) { |
| LogUtils.wtf(LOG_TAG, "Error, mAttachment is null"); |
| } |
| |
| final String cmd; |
| if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { |
| // The operation is different in EAS 14.0 than in earlier versions |
| cmd = "ItemOperations"; |
| } else { |
| final String location; |
| // For Exchange 2003 (EAS 2.5), we have to look for illegal chars in the file name |
| // that EAS sent to us! |
| if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { |
| location = encodeForExchange2003(mAttachment.mLocation); |
| } else { |
| location = mAttachment.mLocation; |
| } |
| cmd = "GetAttachment&AttachmentName=" + location; |
| } |
| return cmd; |
| } |
| |
| @Override |
| protected HttpEntity getRequestEntity() throws IOException { |
| if (mAttachment == null) { |
| LogUtils.wtf(LOG_TAG, "Error, mAttachment is null"); |
| } |
| |
| final HttpEntity entity; |
| final Serializer s = new Serializer(); |
| if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { |
| s.start(Tags.ITEMS_ITEMS).start(Tags.ITEMS_FETCH); |
| s.data(Tags.ITEMS_STORE, "Mailbox"); |
| s.data(Tags.BASE_FILE_REFERENCE, mAttachment.mLocation); |
| s.end().end().done(); // ITEMS_FETCH, ITEMS_ITEMS |
| entity = makeEntity(s); |
| } else { |
| // Older versions of the protocol have the attachment location in the command. |
| entity = null; |
| } |
| return entity; |
| } |
| |
| /** |
| * Close, ignoring errors (as during cleanup) |
| * @param c a Closeable |
| */ |
| private static void close(final Closeable c) { |
| try { |
| c.close(); |
| } catch (IOException e) { |
| LogUtils.e(LOG_TAG, "IOException while cleaning up attachment: %s", e.getMessage()); |
| } |
| } |
| |
| /** |
| * Save away the contentUri for this Attachment and notify listeners |
| */ |
| private boolean finishLoadAttachment(final EmailContent.Attachment attachment, final File file) { |
| final InputStream in; |
| try { |
| in = new FileInputStream(file); |
| } catch (final FileNotFoundException e) { |
| // Unlikely, as we just created it successfully, but log it. |
| LogUtils.e(LOG_TAG, "Could not open attachment file: %s", e.getMessage()); |
| return false; |
| } |
| AttachmentUtilities.saveAttachment(mContext, in, attachment); |
| close(in); |
| return true; |
| } |
| |
| /** |
| * Read the {@link EasResponse} and extract the attachment data, saving it to the provider. |
| * @param response The (successful) {@link EasResponse} containing the attachment data. |
| * @return A status code, 0 is a success, anything negative is an error outlined by constants |
| * in this class or its base class. |
| */ |
| @Override |
| protected int handleResponse(final EasResponse response) { |
| // Some very basic error checking on the response object first. |
| // Our base class should be responsible for checking these errors but if the error |
| // checking is done in the override functions, we can be more specific about |
| // the errors that are being returned to the caller of performOperation(). |
| if (response.isEmpty()) { |
| LogUtils.e(LOG_TAG, "Error, empty response."); |
| return RESULT_NETWORK_PROBLEM; |
| } |
| |
| // This is a 2 step process. |
| // 1. Grab what came over the wire and write it to a temp file on disk. |
| // 2. Move the attachment to its final location. |
| final File tmpFile; |
| try { |
| tmpFile = File.createTempFile("eas_", "tmp", mContext.getCacheDir()); |
| } catch (final IOException e) { |
| LogUtils.e(LOG_TAG, "Could not open temp file: %s", e.getMessage()); |
| return RESULT_NETWORK_PROBLEM; |
| } |
| |
| try { |
| final OutputStream os; |
| try { |
| os = new FileOutputStream(tmpFile); |
| } catch (final FileNotFoundException e) { |
| LogUtils.e(LOG_TAG, "Temp file not found: %s", e.getMessage()); |
| return RESULT_ATTACHMENT_INTERNAL_HANDLING_ERROR; |
| } |
| try { |
| final InputStream is = response.getInputStream(); |
| try { |
| // TODO: Right now we are explictly loading this from a class |
| // that will be deprecated when we move over to EasService. When we start using |
| // our internal class instead, there will be rippling side effect changes that |
| // need to be made when this time comes. |
| final ProgressCallback callback = new ProgressCallback(mCallback, mAttachment); |
| final boolean success; |
| if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { |
| final ItemOperationsParser parser = new ItemOperationsParser(is, os, |
| mAttachment.mSize, callback); |
| parser.parse(); |
| success = (parser.getStatusCode() == 1); |
| } else { |
| final int length = response.getLength(); |
| if (length != 0) { |
| // len > 0 means that Content-Length was set in the headers |
| // len < 0 means "chunked" transfer-encoding |
| ItemOperationsParser.readChunked(is, os, |
| (length < 0) ? mAttachment.mSize : length, callback); |
| } |
| success = true; |
| } |
| // Check that we successfully grabbed what came over the wire... |
| if (!success) { |
| LogUtils.e(LOG_TAG, "Error parsing server response"); |
| return RESULT_ATTACHMENT_RESPONSE_PARSING_ERROR; |
| } |
| // Now finish the process and save to the final destination. |
| final boolean loadResult = finishLoadAttachment(mAttachment, tmpFile); |
| if (!loadResult) { |
| LogUtils.e(LOG_TAG, "Error post processing attachment file."); |
| return RESULT_ATTACHMENT_INTERNAL_HANDLING_ERROR; |
| } |
| } catch (final IOException e) { |
| LogUtils.e(LOG_TAG, "Error handling attachment: %s", e.getMessage()); |
| return RESULT_ATTACHMENT_INTERNAL_HANDLING_ERROR; |
| } finally { |
| close(is); |
| } |
| } finally { |
| close(os); |
| } |
| } finally { |
| tmpFile.delete(); |
| } |
| return RESULT_SUCCESS; |
| } |
| } |