com.android.email.activity.zx.MessageView.java Source code

Java tutorial

Introduction

Here is the source code for com.android.email.activity.zx.MessageView.java

Source

/*
 * Copyright (C) 2008 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.email.activity.zx;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.IOUtils;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Process;
import android.provider.Contacts;
import android.provider.Contacts.Intents;
import android.provider.Contacts.People;
import android.util.Config;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.View.OnClickListener;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.android.text.util.Regex;

import com.android.email.mail.internet.zx.MimeUtility;
import com.android.email.mail.store.zx.LocalStore.LocalAttachmentBodyPart;
import com.android.email.mail.store.zx.LocalStore.LocalMessage;
import com.android.email.mail.zx.Address;
import com.android.email.mail.zx.Message;
import com.android.email.mail.zx.MessagingException;
import com.android.email.mail.zx.Multipart;
import com.android.email.mail.zx.Part;
import com.android.email.mail.zx.Message.RecipientType;
import com.android.email.provider.zx.AttachmentProvider;
import com.android.email.zx.Account;
import com.android.email.zx.Email;
import com.android.email.zx.MessagingController;
import com.android.email.zx.MessagingListener;
import com.android.email.zx.R;
import com.android.email.zx.Utility;

public class MessageView extends Activity implements OnClickListener {
    private static final String EXTRA_ACCOUNT = "com.android.email.MessageView_account";
    private static final String EXTRA_FOLDER = "com.android.email.MessageView_folder";
    private static final String EXTRA_MESSAGE = "com.android.email.MessageView_message";
    private static final String EXTRA_FOLDER_UIDS = "com.android.email.MessageView_folderUids";
    private static final String EXTRA_NEXT = "com.android.email.MessageView_next";

    private static final String[] METHODS_WITH_PRESENCE_PROJECTION = new String[] { People.ContactMethods._ID, // 0
            // 1
    };
    private static final int METHODS_STATUS_COLUMN = 1;

    // regex that matches start of img tag. '.*<(?i)img\s+.*'.
    private static final Pattern IMG_TAG_START_REGEX = Pattern.compile(".*<(?i)img\\s+.*");

    private TextView mSubjectView;
    private TextView mFromView;
    private TextView mDateView;
    private TextView mTimeView;
    private TextView mToView;
    private TextView mCcView;
    private View mCcContainerView;
    private WebView mMessageContentView;
    private LinearLayout mAttachments;
    private ImageView mAttachmentIcon;
    private View mShowPicturesSection;
    private ImageView mSenderPresenceView;

    private Account mAccount;
    private String mFolder;
    private String mMessageUid;
    private ArrayList<String> mFolderUids;

    private Message mMessage;
    private String mNextMessageUid = null;
    private String mPreviousMessageUid = null;

    private java.text.DateFormat mDateFormat;
    private java.text.DateFormat mTimeFormat;

    private Listener mListener = new Listener();
    private MessageViewHandler mHandler = new MessageViewHandler();

    class MessageViewHandler extends Handler {
        private static final int MSG_PROGRESS = 2;
        private static final int MSG_ADD_ATTACHMENT = 3;
        private static final int MSG_SET_ATTACHMENTS_ENABLED = 4;
        private static final int MSG_SET_HEADERS = 5;
        private static final int MSG_NETWORK_ERROR = 6;
        private static final int MSG_ATTACHMENT_SAVED = 7;
        private static final int MSG_ATTACHMENT_NOT_SAVED = 8;
        private static final int MSG_SHOW_SHOW_PICTURES = 9;
        private static final int MSG_FETCHING_ATTACHMENT = 10;
        private static final int MSG_SET_SENDER_PRESENCE = 11;
        private static final int MSG_VIEW_ATTACHMENT_ERROR = 12;

        @Override
        public void handleMessage(android.os.Message msg) {
            switch (msg.what) {
            case MSG_PROGRESS:
                setProgressBarIndeterminateVisibility(msg.arg1 != 0);
                break;
            case MSG_ADD_ATTACHMENT:
                mAttachments.addView((View) msg.obj);
                mAttachments.setVisibility(View.VISIBLE);
                break;
            case MSG_SET_ATTACHMENTS_ENABLED:
                for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
                    Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag();
                    attachment.viewButton.setEnabled(msg.arg1 == 1);
                    attachment.downloadButton.setEnabled(msg.arg1 == 1);
                }
                break;
            case MSG_SET_HEADERS:
                String[] values = (String[]) msg.obj;
                mSubjectView.setText(values[0]);
                mFromView.setText(values[1]);
                mTimeView.setText(values[2]);
                mDateView.setText(values[3]);
                mToView.setText(values[4]);
                mCcView.setText(values[5]);
                mCcContainerView.setVisibility((values[5] != null) ? View.VISIBLE : View.GONE);
                mAttachmentIcon.setVisibility(msg.arg1 == 1 ? View.VISIBLE : View.GONE);
                break;
            case MSG_NETWORK_ERROR:
                Toast.makeText(MessageView.this, R.string.status_network_error, Toast.LENGTH_LONG).show();
                break;
            case MSG_ATTACHMENT_SAVED:
                Toast.makeText(MessageView.this,
                        String.format(getString(R.string.message_view_status_attachment_saved), msg.obj),
                        Toast.LENGTH_LONG).show();
                break;
            case MSG_ATTACHMENT_NOT_SAVED:
                Toast.makeText(MessageView.this, getString(R.string.message_view_status_attachment_not_saved),
                        Toast.LENGTH_LONG).show();
                break;
            case MSG_SHOW_SHOW_PICTURES:
                mShowPicturesSection.setVisibility(msg.arg1 == 1 ? View.VISIBLE : View.GONE);
                break;
            case MSG_FETCHING_ATTACHMENT:
                Toast.makeText(MessageView.this, getString(R.string.message_view_fetching_attachment_toast),
                        Toast.LENGTH_SHORT).show();
                break;
            case MSG_SET_SENDER_PRESENCE:
                updateSenderPresence(msg.arg1);
                break;
            case MSG_VIEW_ATTACHMENT_ERROR:
                Toast.makeText(MessageView.this, getString(R.string.message_view_display_attachment_toast),
                        Toast.LENGTH_SHORT).show();
                break;
            default:
                super.handleMessage(msg);
            }
        }

        public void progress(boolean progress) {
            android.os.Message msg = new android.os.Message();
            msg.what = MSG_PROGRESS;
            msg.arg1 = progress ? 1 : 0;
            sendMessage(msg);
        }

        public void addAttachment(View attachmentView) {
            android.os.Message msg = new android.os.Message();
            msg.what = MSG_ADD_ATTACHMENT;
            msg.obj = attachmentView;
            sendMessage(msg);
        }

        public void setAttachmentsEnabled(boolean enabled) {
            android.os.Message msg = new android.os.Message();
            msg.what = MSG_SET_ATTACHMENTS_ENABLED;
            msg.arg1 = enabled ? 1 : 0;
            sendMessage(msg);
        }

        public void setHeaders(String subject, String from, String time, String date, String to, String cc,
                boolean hasAttachments) {
            android.os.Message msg = new android.os.Message();
            msg.what = MSG_SET_HEADERS;
            msg.arg1 = hasAttachments ? 1 : 0;
            msg.obj = new String[] { subject, from, time, date, to, cc };
            sendMessage(msg);
        }

        public void networkError() {
            sendEmptyMessage(MSG_NETWORK_ERROR);
        }

        public void attachmentSaved(String filename) {
            android.os.Message msg = new android.os.Message();
            msg.what = MSG_ATTACHMENT_SAVED;
            msg.obj = filename;
            sendMessage(msg);
        }

        public void attachmentNotSaved() {
            sendEmptyMessage(MSG_ATTACHMENT_NOT_SAVED);
        }

        public void fetchingAttachment() {
            sendEmptyMessage(MSG_FETCHING_ATTACHMENT);
        }

        public void showShowPictures(boolean show) {
            android.os.Message msg = new android.os.Message();
            msg.what = MSG_SHOW_SHOW_PICTURES;
            msg.arg1 = show ? 1 : 0;
            sendMessage(msg);
        }

        public void setSenderPresence(int presenceIconId) {
            android.os.Message.obtain(this, MSG_SET_SENDER_PRESENCE, presenceIconId, 0).sendToTarget();
        }

        public void attachmentViewError() {
            sendEmptyMessage(MSG_VIEW_ATTACHMENT_ERROR);
        }
    }

    /**
     * Encapsulates known information about a single attachment.
     */
    private static class Attachment {
        public String name;
        public String contentType;
        public long size;
        public LocalAttachmentBodyPart part;
        public Button viewButton;
        public Button downloadButton;
        public ImageView iconView;
    }

    public static void actionView(Context context, Account account, String folder, String messageUid,
            ArrayList<String> folderUids) {
        actionView(context, account, folder, messageUid, folderUids, null);
    }

    public static void actionView(Context context, Account account, String folder, String messageUid,
            ArrayList<String> folderUids, Bundle extras) {
        Intent i = new Intent(context, MessageView.class);
        i.putExtra(EXTRA_ACCOUNT, account);
        i.putExtra(EXTRA_FOLDER, folder);
        i.putExtra(EXTRA_MESSAGE, messageUid);
        i.putExtra(EXTRA_FOLDER_UIDS, folderUids);
        if (extras != null) {
            i.putExtras(extras);
        }
        context.startActivity(i);
    }

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);

        setContentView(R.layout.message_view);

        mSubjectView = (TextView) findViewById(R.id.subject);
        mFromView = (TextView) findViewById(R.id.from);
        mToView = (TextView) findViewById(R.id.to);
        mCcView = (TextView) findViewById(R.id.cc);
        mCcContainerView = findViewById(R.id.cc_container);
        mDateView = (TextView) findViewById(R.id.date);
        mTimeView = (TextView) findViewById(R.id.time);
        mMessageContentView = (WebView) findViewById(R.id.message_content);
        mAttachments = (LinearLayout) findViewById(R.id.attachments);
        mAttachmentIcon = (ImageView) findViewById(R.id.attachment);
        mShowPicturesSection = findViewById(R.id.show_pictures_section);
        mSenderPresenceView = (ImageView) findViewById(R.id.presence);

        mMessageContentView.setVerticalScrollBarEnabled(false);
        mAttachments.setVisibility(View.GONE);
        mAttachmentIcon.setVisibility(View.GONE);

        mFromView.setOnClickListener(this);
        mSenderPresenceView.setOnClickListener(this);
        findViewById(R.id.reply).setOnClickListener(this);
        findViewById(R.id.reply_all).setOnClickListener(this);
        findViewById(R.id.delete).setOnClickListener(this);
        findViewById(R.id.show_pictures).setOnClickListener(this);

        mMessageContentView.getSettings().setBlockNetworkImage(true);
        mMessageContentView.getSettings().setSupportZoom(false);

        setTitle("");

        mDateFormat = android.text.format.DateFormat.getDateFormat(this); // short format
        mTimeFormat = android.text.format.DateFormat.getTimeFormat(this); // 12/24 date format

        Intent intent = getIntent();
        mAccount = (Account) intent.getSerializableExtra(EXTRA_ACCOUNT);
        mFolder = intent.getStringExtra(EXTRA_FOLDER);
        mMessageUid = intent.getStringExtra(EXTRA_MESSAGE);
        mFolderUids = intent.getStringArrayListExtra(EXTRA_FOLDER_UIDS);

        View next = findViewById(R.id.next);
        View previous = findViewById(R.id.previous);
        /*
         * Next and Previous Message are not shown in landscape mode, so
         * we need to check before we use them.
         */
        if (next != null && previous != null) {
            next.setOnClickListener(this);
            previous.setOnClickListener(this);

            findSurroundingMessagesUid();

            previous.setVisibility(mPreviousMessageUid != null ? View.VISIBLE : View.GONE);
            next.setVisibility(mNextMessageUid != null ? View.VISIBLE : View.GONE);

            boolean goNext = intent.getBooleanExtra(EXTRA_NEXT, false);
            if (goNext) {
                next.requestFocus();
            }
        }

        MessagingController.getInstance(getApplication()).addListener(mListener);
        new Thread() {
            @Override
            public void run() {
                // TODO this is a spot that should be eventually handled by a MessagingController
                // thread pool. We want it in a thread but it can't be blocked by the normal
                // synchronization stuff in MC.
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                MessagingController.getInstance(getApplication()).loadMessageForView(mAccount, mFolder, mMessageUid,
                        mListener);
            }
        }.start();
    }

    private void findSurroundingMessagesUid() {
        for (int i = 0, count = mFolderUids.size(); i < count; i++) {
            String messageUid = mFolderUids.get(i);
            if (messageUid.equals(mMessageUid)) {
                if (i != 0) {
                    mPreviousMessageUid = mFolderUids.get(i - 1);
                }

                if (i != count - 1) {
                    mNextMessageUid = mFolderUids.get(i + 1);
                }
                break;
            }
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        MessagingController.getInstance(getApplication()).addListener(mListener);
        if (mMessage != null) {
            startPresenceCheck();
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        MessagingController.getInstance(getApplication()).removeListener(mListener);
    }

    /**
     * We override onDestroy to make sure that the WebView gets explicitly destroyed.
     * Otherwise it can leak native references.
     */
    @Override
    public void onDestroy() {
        super.onDestroy();
        mMessageContentView.destroy();
        mMessageContentView = null;
    }

    private void onDelete() {
        if (mMessage != null) {
            MessagingController.getInstance(getApplication()).deleteMessage(mAccount, mFolder, mMessage, null);
            Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();

            // Remove this message's Uid locally
            mFolderUids.remove(mMessage.getUid());
            // Check if we have previous/next messages available before choosing
            // which one to display
            findSurroundingMessagesUid();

            if (mPreviousMessageUid != null) {
                onPrevious();
            } else if (mNextMessageUid != null) {
                onNext();
            } else {
                finish();
            }
        }
    }

    private void onClickSender() {
        if (mMessage != null) {
            try {
                Address senderEmail = mMessage.getFrom()[0];
                Uri contactUri = Uri.fromParts("mailto", senderEmail.getAddress(), null);

                Intent contactIntent = new Intent(Contacts.Intents.SHOW_OR_CREATE_CONTACT);
                contactIntent.setData(contactUri);

                // Pass along full E-mail string for possible create dialog  
                contactIntent.putExtra(Contacts.Intents.EXTRA_CREATE_DESCRIPTION, senderEmail.toString());

                // Only provide personal name hint if we have one
                String senderPersonal = senderEmail.getPersonal();
                if (senderPersonal != null) {
                    contactIntent.putExtra(Intents.Insert.NAME, senderPersonal);
                }

                startActivity(contactIntent);
            } catch (MessagingException me) {
                if (Config.LOGV) {
                    Log.v(Email.LOG_TAG, "loadMessageForViewHeadersAvailable", me);
                }
            }
        }
    }

    private void onReply() {
        if (mMessage != null) {
            MessageCompose.actionReply(this, mAccount, mMessage, false);
            finish();
        }
    }

    private void onReplyAll() {
        if (mMessage != null) {
            MessageCompose.actionReply(this, mAccount, mMessage, true);
            finish();
        }
    }

    private void onForward() {
        if (mMessage != null) {
            MessageCompose.actionForward(this, mAccount, mMessage);
            finish();
        }
    }

    private void onNext() {
        Bundle extras = new Bundle(1);
        extras.putBoolean(EXTRA_NEXT, true);
        MessageView.actionView(this, mAccount, mFolder, mNextMessageUid, mFolderUids, extras);
        finish();
    }

    private void onPrevious() {
        MessageView.actionView(this, mAccount, mFolder, mPreviousMessageUid, mFolderUids);
        finish();
    }

    private void onMarkAsUnread() {
        if (mMessage != null) {
            MessagingController.getInstance(getApplication()).markMessageRead(mAccount, mFolder, mMessage.getUid(),
                    false);
        }
    }

    /**
     * Creates a unique file in the given directory by appending a hyphen
     * and a number to the given filename.
     * @param directory
     * @param filename
     * @return a new File object, or null if one could not be created
     */
    private File createUniqueFile(File directory, String filename) {
        File file = new File(directory, filename);
        if (!file.exists()) {
            return file;
        }
        // Get the extension of the file, if any.
        int index = filename.lastIndexOf('.');
        String format;
        if (index != -1) {
            String name = filename.substring(0, index);
            String extension = filename.substring(index);
            format = name + "-%d" + extension;
        } else {
            format = filename + "-%d";
        }
        for (int i = 2; i < Integer.MAX_VALUE; i++) {
            file = new File(directory, String.format(format, i));
            if (!file.exists()) {
                return file;
            }
        }
        return null;
    }

    private void onDownloadAttachment(Attachment attachment) {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            /*
             * Abort early if there's no place to save the attachment. We don't want to spend
             * the time downloading it and then abort.
             */
            Toast.makeText(this, getString(R.string.message_view_status_attachment_not_saved), Toast.LENGTH_SHORT)
                    .show();
            return;
        }
        MessagingController.getInstance(getApplication()).loadAttachment(mAccount, mMessage, attachment.part,
                new Object[] { true, attachment }, mListener);
    }

    private void onViewAttachment(Attachment attachment) {
        MessagingController.getInstance(getApplication()).loadAttachment(mAccount, mMessage, attachment.part,
                new Object[] { false, attachment }, mListener);
    }

    private void onShowPictures() {
        if (mMessage != null) {
            mMessageContentView.getSettings().setBlockNetworkImage(false);
            mShowPicturesSection.setVisibility(View.GONE);
        }
    }

    public void onClick(View view) {
        switch (view.getId()) {
        case R.id.from:
        case R.id.presence:
            onClickSender();
            break;
        case R.id.reply:
            onReply();
            break;
        case R.id.reply_all:
            onReplyAll();
            break;
        case R.id.delete:
            onDelete();
            break;
        case R.id.next:
            onNext();
            break;
        case R.id.previous:
            onPrevious();
            break;
        case R.id.download:
            onDownloadAttachment((Attachment) view.getTag());
            break;
        case R.id.view:
            onViewAttachment((Attachment) view.getTag());
            break;
        case R.id.show_pictures:
            onShowPictures();
            break;
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        boolean handled = handleMenuItem(item.getItemId());
        if (!handled) {
            handled = super.onOptionsItemSelected(item);
        }
        return handled;
    }

    /**
     * This is the core functionality of onOptionsItemSelected() but broken out and exposed
     * for testing purposes (because it's annoying to mock a MenuItem).
     * 
     * @param menuItemId id that was clicked
     * @return true if handled here
     */
    /* package */ boolean handleMenuItem(int menuItemId) {
        switch (menuItemId) {
        case R.id.delete:
            onDelete();
            break;
        case R.id.reply:
            onReply();
            break;
        case R.id.reply_all:
            onReplyAll();
            break;
        case R.id.forward:
            onForward();
            break;
        case R.id.mark_as_unread:
            onMarkAsUnread();
            break;
        default:
            return false;
        }
        return true;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.message_view_option, menu);
        return true;
    }

    private Bitmap getPreviewIcon(Attachment attachment) {
        try {
            return BitmapFactory.decodeStream(getContentResolver().openInputStream(AttachmentProvider
                    .getAttachmentThumbnailUri(mAccount, attachment.part.getAttachmentId(), 62, 62)));
        } catch (Exception e) {
            /*
             * We don't care what happened, we just return null for the preview icon.
             */
            return null;
        }
    }

    /*
     * Formats the given size as a String in bytes, kB, MB or GB with a single digit
     * of precision. Ex: 12,315,000 = 12.3 MB
     */
    public static String formatSize(float size) {
        long kb = 1024;
        long mb = (kb * 1024);
        long gb = (mb * 1024);
        if (size < kb) {
            return String.format("%d bytes", (int) size);
        } else if (size < mb) {
            return String.format("%.1f kB", size / kb);
        } else if (size < gb) {
            return String.format("%.1f MB", size / mb);
        } else {
            return String.format("%.1f GB", size / gb);
        }
    }

    /**
     * Resolve content-id reference in src attribute of img tag to AttachmentProvider's
     * content uri.  This method calls itself recursively at most the number of
     * LocalAttachmentPart that mime type is image and has content id.
     * The attribute src="cid:content_id" is resolved as src="content://...".
     * This method is package scope for testing purpose.
     *
     * @param text html email text
     * @param part mime part which may contain inline image
     * @return html text in which src attribute of img tag may be replaced with content uri
     */
    /* package */ String resolveInlineImage(String text, Part part, int depth) throws MessagingException {
        // avoid too deep recursive call.
        if (depth >= 10) {
            return text;
        }
        String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
        String contentId = part.getContentId();
        if (contentType.startsWith("image/") && contentId != null && part instanceof LocalAttachmentBodyPart) {
            LocalAttachmentBodyPart attachment = (LocalAttachmentBodyPart) part;
            Uri contentUri = AttachmentProvider.getAttachmentUri(mAccount, attachment.getAttachmentId());
            if (contentUri != null) {
                // Regexp which matches ' src="cid:contentId"'.
                String contentIdRe = "\\s+(?i)src=\"cid(?-i):\\Q" + contentId + "\\E\"";
                // Replace all occurrences of src attribute with ' src="content://contentUri"'.
                text = text.replaceAll(contentIdRe, " src=\"" + contentUri + "\"");
            }
        }

        if (part.getBody() instanceof Multipart) {
            Multipart mp = (Multipart) part.getBody();
            for (int i = 0; i < mp.getCount(); i++) {
                text = resolveInlineImage(text, mp.getBodyPart(i), depth + 1);
            }
        }

        return text;
    }

    private void renderAttachments(Part part, int depth) throws MessagingException {
        String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
        String name = MimeUtility.getHeaderParameter(contentType, "name");
        if (name != null) {
            /*
             * We're guaranteed size because LocalStore.fetch puts it there.
             */
            String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
            int size = Integer.parseInt(MimeUtility.getHeaderParameter(contentDisposition, "size"));

            Attachment attachment = new Attachment();
            attachment.size = size;
            attachment.contentType = part.getMimeType();
            attachment.name = name;
            attachment.part = (LocalAttachmentBodyPart) part;

            LayoutInflater inflater = getLayoutInflater();
            View view = inflater.inflate(R.layout.message_view_attachment, null);

            TextView attachmentName = (TextView) view.findViewById(R.id.attachment_name);
            TextView attachmentInfo = (TextView) view.findViewById(R.id.attachment_info);
            ImageView attachmentIcon = (ImageView) view.findViewById(R.id.attachment_icon);
            Button attachmentView = (Button) view.findViewById(R.id.view);
            Button attachmentDownload = (Button) view.findViewById(R.id.download);

            if ((!MimeUtility.mimeTypeMatches(attachment.contentType, Email.ACCEPTABLE_ATTACHMENT_VIEW_TYPES))
                    || (MimeUtility.mimeTypeMatches(attachment.contentType,
                            Email.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) {
                attachmentView.setVisibility(View.GONE);
            }
            if ((!MimeUtility.mimeTypeMatches(attachment.contentType, Email.ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))
                    || (MimeUtility.mimeTypeMatches(attachment.contentType,
                            Email.UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))) {
                attachmentDownload.setVisibility(View.GONE);
            }

            if (attachment.size > Email.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
                attachmentView.setVisibility(View.GONE);
                attachmentDownload.setVisibility(View.GONE);
            }

            attachment.viewButton = attachmentView;
            attachment.downloadButton = attachmentDownload;
            attachment.iconView = attachmentIcon;

            view.setTag(attachment);
            attachmentView.setOnClickListener(this);
            attachmentView.setTag(attachment);
            attachmentDownload.setOnClickListener(this);
            attachmentDownload.setTag(attachment);

            attachmentName.setText(name);
            attachmentInfo.setText(formatSize(size));

            Bitmap previewIcon = getPreviewIcon(attachment);
            if (previewIcon != null) {
                attachmentIcon.setImageBitmap(previewIcon);
            }

            mHandler.addAttachment(view);
        }

        if (part.getBody() instanceof Multipart) {
            Multipart mp = (Multipart) part.getBody();
            for (int i = 0; i < mp.getCount(); i++) {
                renderAttachments(mp.getBodyPart(i), depth + 1);
            }
        }
    }

    /**
     * Launch a thread (because of cross-process DB lookup) to check presence of the sender of the
     * message.  When that thread completes, update the UI.
     * 
     * This must only be called when mMessage is null (it will hide presence indications) or when
     * mMessage has already seen its headers loaded.
     * 
     * Note:  This is just a polling operation.  A more advanced solution would be to keep the
     * cursor open and respond to presence status updates (in the form of content change
     * notifications).  However, because presence changes fairly slowly compared to the duration
     * of viewing a single message, a simple poll at message load (and onResume) should be
     * sufficient.
     */
    private void startPresenceCheck() {
        String email = null;
        try {
            if (mMessage != null) {
                Address sender = mMessage.getFrom()[0];
                email = sender.getAddress();
            }
        } catch (MessagingException me) {
        }
        if (email == null) {
            mHandler.setSenderPresence(0);
            return;
        }
        final String senderEmail = email;

        new Thread() {
            @Override
            public void run() {
                Cursor methodsCursor = getContentResolver().query(
                        Uri.withAppendedPath(Contacts.ContactMethods.CONTENT_URI, "with_presence"),
                        METHODS_WITH_PRESENCE_PROJECTION, Contacts.ContactMethods.DATA + "=?",
                        new String[] { senderEmail }, null);

                int presenceIcon = 0;

                if (methodsCursor != null) {
                    if (methodsCursor.moveToFirst() && !methodsCursor.isNull(METHODS_STATUS_COLUMN)) {
                    }
                    methodsCursor.close();
                }

                mHandler.setSenderPresence(presenceIcon);
            }
        }.start();
    }

    /**
     * Update the actual UI.  Must be called from main thread (or handler)
     * @param presenceIconId the presence of the sender, 0 for "unknown"
     */
    private void updateSenderPresence(int presenceIconId) {
        if (presenceIconId == 0) {
            // This is a placeholder used for "unknown" presence, including signed off,
            // no presence relationship.
            presenceIconId = R.drawable.presence_inactive;
        }
        mSenderPresenceView.setImageResource(presenceIconId);
    }

    class Listener extends MessagingListener {
        @Override
        public void loadMessageForViewHeadersAvailable(Account account, String folder, String uid,
                final Message message) {
            MessageView.this.mMessage = message;
            try {
                String subjectText = message.getSubject();
                String fromText = Address.toFriendly(message.getFrom());
                Date sentDate = message.getSentDate();
                String timeText = mTimeFormat.format(sentDate);
                String dateText = Utility.isDateToday(sentDate) ? null : mDateFormat.format(sentDate);
                String toText = Address.toFriendly(message.getRecipients(RecipientType.TO));
                String ccText = Address.toFriendly(message.getRecipients(RecipientType.CC));
                boolean hasAttachments = ((LocalMessage) message).getAttachmentCount() > 0;
                mHandler.setHeaders(subjectText, fromText, timeText, dateText, toText, ccText, hasAttachments);
                startPresenceCheck();
            } catch (MessagingException me) {
                if (Config.LOGV) {
                    Log.v(Email.LOG_TAG, "loadMessageForViewHeadersAvailable", me);
                }
            }
        }

        @Override
        public void loadMessageForViewBodyAvailable(Account account, String folder, String uid, Message message) {
            MessageView.this.mMessage = message;
            try {
                Part part = MimeUtility.findFirstPartByMimeType(mMessage, "text/html");
                if (part == null) {
                    part = MimeUtility.findFirstPartByMimeType(mMessage, "text/plain");
                }
                if (part != null) {
                    String text = MimeUtility.getTextFromPart(part);
                    if (part.getMimeType().equalsIgnoreCase("text/html")) {
                        text = resolveInlineImage(text, mMessage, 0);
                    } else {
                        /*
                         * Linkify the plain text and convert it to HTML by replacing
                         * \r?\n with <br> and adding a html/body wrapper.
                         */
                        Matcher m = Regex.WEB_URL_PATTERN.matcher(text);
                        StringBuffer sb = new StringBuffer();
                        while (m.find()) {
                            int start = m.start();
                            if (start != 0 && text.charAt(start - 1) != '@') {
                                m.appendReplacement(sb, "<a href=\"$0\">$0</a>");
                            } else {
                                m.appendReplacement(sb, "$0");
                            }
                        }
                        m.appendTail(sb);
                        text = sb.toString().replaceAll("\r?\n", "<br>");
                        text = "<html><body>" + text + "</body></html>";
                    }

                    /*
                     * TODO consider how to get background images and a million other things
                     * that HTML allows.
                     */
                    // Check if text contains img tag.
                    if (IMG_TAG_START_REGEX.matcher(text).matches()) {
                        mHandler.showShowPictures(true);
                    }

                    mMessageContentView.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null);
                } else {
                    mMessageContentView.loadUrl("file:///android_asset/empty.html");
                }
                renderAttachments(mMessage, 0);
            } catch (Exception e) {
                if (Config.LOGV) {
                    Log.v(Email.LOG_TAG, "loadMessageForViewBodyAvailable", e);
                }
            }
        }

        @Override
        public void loadMessageForViewFailed(Account account, String folder, String uid, final String message) {
            mHandler.post(new Runnable() {
                public void run() {
                    setProgressBarIndeterminateVisibility(false);
                    mHandler.networkError();
                    mMessageContentView.loadUrl("file:///android_asset/empty.html");
                }
            });
        }

        @Override
        public void loadMessageForViewFinished(Account account, String folder, String uid, Message message) {
            mHandler.post(new Runnable() {
                public void run() {
                    setProgressBarIndeterminateVisibility(false);
                }
            });
        }

        @Override
        public void loadMessageForViewStarted(Account account, String folder, String uid) {
            mHandler.post(new Runnable() {
                public void run() {
                    mMessageContentView.loadUrl("file:///android_asset/loading.html");
                    setProgressBarIndeterminateVisibility(true);
                }
            });
        }

        @Override
        public void loadAttachmentStarted(Account account, Message message, Part part, Object tag,
                boolean requiresDownload) {
            mHandler.setAttachmentsEnabled(false);
            mHandler.progress(true);
            if (requiresDownload) {
                mHandler.fetchingAttachment();
            }
        }

        @Override
        public void loadAttachmentFinished(Account account, Message message, Part part, Object tag) {
            mHandler.setAttachmentsEnabled(true);
            mHandler.progress(false);

            Object[] params = (Object[]) tag;
            boolean download = (Boolean) params[0];
            Attachment attachment = (Attachment) params[1];

            if (download) {
                try {
                    File file = createUniqueFile(Environment.getExternalStorageDirectory(), attachment.name);
                    Uri uri = AttachmentProvider.getAttachmentUri(mAccount, attachment.part.getAttachmentId());
                    InputStream in = getContentResolver().openInputStream(uri);
                    OutputStream out = new FileOutputStream(file);
                    IOUtils.copy(in, out);
                    out.flush();
                    out.close();
                    in.close();
                    mHandler.attachmentSaved(file.getName());
                    new MediaScannerNotifier(MessageView.this, file, mHandler);
                } catch (IOException ioe) {
                    mHandler.attachmentNotSaved();
                }
            } else {
                try {
                    Uri uri = AttachmentProvider.getAttachmentUri(mAccount, attachment.part.getAttachmentId());
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    intent.setData(uri);
                    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    startActivity(intent);
                } catch (ActivityNotFoundException e) {
                    mHandler.attachmentViewError();
                    // TODO: Add a proper warning message (and lots of upstream cleanup to prevent 
                    // it from happening) in the next release.
                }
            }
        }

        @Override
        public void loadAttachmentFailed(Account account, Message message, Part part, Object tag, String reason) {
            mHandler.setAttachmentsEnabled(true);
            mHandler.progress(false);
            mHandler.networkError();
        }
    }

    /**
     * This notifier is created after an attachment completes downloaded.  It attaches to the
     * media scanner and waits to handle the completion of the scan.  At that point it tries
     * to start an ACTION_VIEW activity for the attachment.
    */
    private static class MediaScannerNotifier implements MediaScannerConnectionClient {
        private Context mContext;
        private MediaScannerConnection mConnection;
        private File mFile;
        MessageViewHandler mHandler;

        public MediaScannerNotifier(Context context, File file, MessageViewHandler handler) {
            mContext = context;
            mFile = file;
            mHandler = handler;
            mConnection = new MediaScannerConnection(context, this);
            mConnection.connect();
        }

        public void onMediaScannerConnected() {
            mConnection.scanFile(mFile.getAbsolutePath(), null);
        }

        public void onScanCompleted(String path, Uri uri) {
            try {
                if (uri != null) {
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    intent.setData(uri);
                    mContext.startActivity(intent);
                }
            } catch (ActivityNotFoundException e) {
                mHandler.attachmentViewError();
                // TODO: Add a proper warning message (and lots of upstream cleanup to prevent 
                // it from happening) in the next release.
            } finally {
                mConnection.disconnect();
                mContext = null;
                mHandler = null;
            }
        }
    }
}