FileDocCategorySizeDatePackage
MessageView.javaAPI DocAndroid 1.5 API43523Wed May 06 22:42:46 BST 2009com.android.email.activity

MessageView

public class MessageView extends android.app.Activity implements android.view.View.OnClickListener

Fields Summary
private static final String
EXTRA_ACCOUNT
private static final String
EXTRA_FOLDER
private static final String
EXTRA_MESSAGE
private static final String
EXTRA_FOLDER_UIDS
private static final String
EXTRA_NEXT
private static final String[]
METHODS_WITH_PRESENCE_PROJECTION
private static final int
METHODS_STATUS_COLUMN
private static final Pattern
IMG_TAG_START_REGEX
private android.widget.TextView
mSubjectView
private android.widget.TextView
mFromView
private android.widget.TextView
mDateView
private android.widget.TextView
mTimeView
private android.widget.TextView
mToView
private android.widget.TextView
mCcView
private android.view.View
mCcContainerView
private android.webkit.WebView
mMessageContentView
private android.widget.LinearLayout
mAttachments
private android.widget.ImageView
mAttachmentIcon
private android.view.View
mShowPicturesSection
private android.widget.ImageView
mSenderPresenceView
private com.android.email.Account
mAccount
private String
mFolder
private String
mMessageUid
private ArrayList
mFolderUids
private com.android.email.mail.Message
mMessage
private String
mNextMessageUid
private String
mPreviousMessageUid
private DateFormat
mDateFormat
private DateFormat
mTimeFormat
private Listener
mListener
private MessageViewHandler
mHandler
Constructors Summary
Methods Summary
public static voidactionView(android.content.Context context, com.android.email.Account account, java.lang.String folder, java.lang.String messageUid, java.util.ArrayList folderUids)

        actionView(context, account, folder, messageUid, folderUids, null);
    
public static voidactionView(android.content.Context context, com.android.email.Account account, java.lang.String folder, java.lang.String messageUid, java.util.ArrayList folderUids, android.os.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);
     
private java.io.FilecreateUniqueFile(java.io.File directory, java.lang.String filename)
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

        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 voidfindSurroundingMessagesUid()

        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;
            }
        }
    
public static java.lang.StringformatSize(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);
        }
    
private android.graphics.BitmapgetPreviewIcon(com.android.email.activity.MessageView$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;
        }
    
booleanhandleMenuItem(int menuItemId)
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

       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;
   
public voidonClick(android.view.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;
        }
    
private voidonClickSender()

        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);
                }
            }
        }
    
public voidonCreate(android.os.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();
    
public booleanonCreateOptionsMenu(android.view.Menu menu)

        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.message_view_option, menu);
        return true;
    
private voidonDelete()

        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();
            }
        }
    
public voidonDestroy()
We override onDestroy to make sure that the WebView gets explicitly destroyed. Otherwise it can leak native references.

        super.onDestroy();
        mMessageContentView.destroy();
        mMessageContentView = null;
    
private voidonDownloadAttachment(com.android.email.activity.MessageView$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 voidonForward()

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

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

        Bundle extras = new Bundle(1);
        extras.putBoolean(EXTRA_NEXT, true);
        MessageView.actionView(this, mAccount, mFolder, mNextMessageUid, mFolderUids, extras);
        finish();
    
public booleanonOptionsItemSelected(android.view.MenuItem item)

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

        super.onPause();
        MessagingController.getInstance(getApplication()).removeListener(mListener);
    
private voidonPrevious()

        MessageView.actionView(this, mAccount, mFolder, mPreviousMessageUid, mFolderUids);
        finish();
    
private voidonReply()

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

        if (mMessage != null) {
            MessageCompose.actionReply(this, mAccount, mMessage, true);
            finish();
        }
    
public voidonResume()

        super.onResume();
        MessagingController.getInstance(getApplication()).addListener(mListener);
        if (mMessage != null) {
            startPresenceCheck();
        }
    
private voidonShowPictures()

        if (mMessage != null) {
            mMessageContentView.getSettings().setBlockNetworkImage(false);
            mShowPicturesSection.setVisibility(View.GONE);
        }
    
private voidonViewAttachment(com.android.email.activity.MessageView$Attachment attachment)

        MessagingController.getInstance(getApplication()).loadAttachment(
                mAccount,
                mMessage,
                attachment.part,
                new Object[] { false, attachment },
                mListener);
    
private voidrenderAttachments(com.android.email.mail.Part part, int depth)

        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);
            }
        }
    
java.lang.StringresolveInlineImage(java.lang.String text, com.android.email.mail.Part part, int depth)
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

        // 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 voidstartPresenceCheck()
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.

        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)) {
                        presenceIcon = Presence.getPresenceIconResourceId(
                                methodsCursor.getInt(METHODS_STATUS_COLUMN));
                    }
                    methodsCursor.close();
                }

                mHandler.setSenderPresence(presenceIcon);
            }
        }.start();
    
private voidupdateSenderPresence(int presenceIconId)
Update the actual UI. Must be called from main thread (or handler)

param
presenceIconId the presence of the sender, 0 for "unknown"

        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);