replicant-packages_apps_Email/src/com/android/email/activity/MessageView.java

1101 lines
41 KiB
Java

/*
* 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;
import com.android.email.Account;
import com.android.email.Email;
import com.android.email.MessagingController;
import com.android.email.MessagingListener;
import com.android.email.R;
import com.android.email.Utility;
import com.android.email.mail.Address;
import com.android.email.mail.Message;
import com.android.email.mail.MessagingException;
import com.android.email.mail.Multipart;
import com.android.email.mail.Part;
import com.android.email.mail.Message.RecipientType;
import com.android.email.mail.internet.MimeUtility;
import com.android.email.mail.store.LocalStore.LocalAttachmentBodyPart;
import com.android.email.mail.store.LocalStore.LocalMessage;
import com.android.email.provider.AttachmentProvider;
import org.apache.commons.io.IOUtils;
import android.app.Activity;
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.provider.Contacts.Presence;
import android.text.util.Regex;
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 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;
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
People.PRESENCE_STATUS, // 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;
@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;
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();
}
}
/**
* 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)) {
presenceIcon = Presence.getPresenceIconResourceId(
methodsCursor.getInt(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);
}
catch (IOException ioe) {
mHandler.attachmentNotSaved();
}
}
else {
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);
}
}
@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;
public MediaScannerNotifier(Context context, File file) {
mContext = context;
mFile = file;
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);
}
} finally {
mConnection.disconnect();
mContext = null;
}
}
}
}