1632 lines
64 KiB
Java
1632 lines
64 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.common.Patterns;
|
|
import com.android.email.Controller;
|
|
import com.android.email.Email;
|
|
import com.android.email.R;
|
|
import com.android.email.Utility;
|
|
import com.android.email.mail.Address;
|
|
import com.android.email.mail.MessagingException;
|
|
import com.android.email.mail.internet.EmailHtmlUtil;
|
|
import com.android.email.mail.internet.MimeUtility;
|
|
import com.android.email.provider.AttachmentProvider;
|
|
import com.android.email.provider.EmailContent;
|
|
import com.android.email.provider.EmailContent.Attachment;
|
|
import com.android.email.provider.EmailContent.Body;
|
|
import com.android.email.provider.EmailContent.BodyColumns;
|
|
import com.android.email.provider.EmailContent.Message;
|
|
import com.android.email.service.EmailServiceConstants;
|
|
|
|
import org.apache.commons.io.IOUtils;
|
|
|
|
import android.app.Activity;
|
|
import android.app.ProgressDialog;
|
|
import android.content.ActivityNotFoundException;
|
|
import android.content.ContentResolver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.database.ContentObserver;
|
|
import android.database.Cursor;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.BitmapFactory;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.media.MediaScannerConnection;
|
|
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
|
|
import android.net.Uri;
|
|
import android.os.AsyncTask;
|
|
import android.os.Bundle;
|
|
import android.os.Environment;
|
|
import android.os.Handler;
|
|
import android.provider.Browser;
|
|
import android.provider.ContactsContract;
|
|
import android.provider.ContactsContract.CommonDataKinds;
|
|
import android.provider.ContactsContract.Contacts;
|
|
import android.provider.ContactsContract.QuickContact;
|
|
import android.provider.ContactsContract.StatusUpdates;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.view.LayoutInflater;
|
|
import android.view.Menu;
|
|
import android.view.MenuItem;
|
|
import android.view.View;
|
|
import android.view.View.OnClickListener;
|
|
import android.webkit.WebView;
|
|
import android.webkit.WebViewClient;
|
|
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.Date;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
public class MessageView extends Activity implements OnClickListener {
|
|
private static final String EXTRA_MESSAGE_ID = "com.android.email.MessageView_message_id";
|
|
private static final String EXTRA_MAILBOX_ID = "com.android.email.MessageView_mailbox_id";
|
|
/* package */ static final String EXTRA_DISABLE_REPLY = "com.android.email.MessageView_disable_reply";
|
|
|
|
// for saveInstanceState()
|
|
private static final String STATE_MESSAGE_ID = "messageId";
|
|
|
|
// Regex that matches start of img tag. '<(?i)img\s+'.
|
|
private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+");
|
|
// Regex that matches Web URL protocol part as case insensitive.
|
|
private static final Pattern WEB_URL_PROTOCOL = Pattern.compile("(?i)http|https://");
|
|
|
|
// Support for LoadBodyTask
|
|
private static final String[] BODY_CONTENT_PROJECTION = new String[] {
|
|
Body.RECORD_ID, BodyColumns.MESSAGE_KEY,
|
|
BodyColumns.HTML_CONTENT, BodyColumns.TEXT_CONTENT
|
|
};
|
|
|
|
private static final String[] PRESENCE_STATUS_PROJECTION =
|
|
new String[] { Contacts.CONTACT_PRESENCE };
|
|
|
|
private static final int BODY_CONTENT_COLUMN_RECORD_ID = 0;
|
|
private static final int BODY_CONTENT_COLUMN_MESSAGE_KEY = 1;
|
|
private static final int BODY_CONTENT_COLUMN_HTML_CONTENT = 2;
|
|
private static final int BODY_CONTENT_COLUMN_TEXT_CONTENT = 3;
|
|
|
|
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 ImageView mFavoriteIcon;
|
|
private View mShowPicturesSection;
|
|
private ImageView mSenderPresenceView;
|
|
private ProgressDialog mProgressDialog;
|
|
private View mScrollView;
|
|
|
|
private long mAccountId;
|
|
private long mMessageId;
|
|
private long mMailboxId;
|
|
private Message mMessage;
|
|
private long mWaitForLoadMessageId;
|
|
|
|
private LoadMessageTask mLoadMessageTask;
|
|
private LoadBodyTask mLoadBodyTask;
|
|
private LoadAttachmentsTask mLoadAttachmentsTask;
|
|
private PresenceCheckTask mPresenceCheckTask;
|
|
|
|
private long mLoadAttachmentId; // the attachment being saved/viewed
|
|
private boolean mLoadAttachmentSave; // if true, saving - if false, viewing
|
|
private String mLoadAttachmentName; // the display name
|
|
|
|
private java.text.DateFormat mDateFormat;
|
|
private java.text.DateFormat mTimeFormat;
|
|
|
|
private Drawable mFavoriteIconOn;
|
|
private Drawable mFavoriteIconOff;
|
|
|
|
private MessageViewHandler mHandler = new MessageViewHandler();
|
|
private Controller mController;
|
|
private ControllerResults mControllerCallback = new ControllerResults();
|
|
|
|
private View mMoveToNewer;
|
|
private View mMoveToOlder;
|
|
private LoadMessageListTask mLoadMessageListTask;
|
|
private Cursor mMessageListCursor;
|
|
private ContentObserver mCursorObserver;
|
|
|
|
// contains the HTML body. Is used by LoadAttachmentTask to display inline images.
|
|
private String mHtmlText;
|
|
|
|
// this is true when reply & forward are disabled, such as messages in the trash
|
|
private boolean mDisableReplyAndForward;
|
|
|
|
class MessageViewHandler extends Handler {
|
|
private static final int MSG_PROGRESS = 1;
|
|
private static final int MSG_ATTACHMENT_PROGRESS = 2;
|
|
private static final int MSG_LOAD_CONTENT_URI = 3;
|
|
private static final int MSG_SET_ATTACHMENTS_ENABLED = 4;
|
|
private static final int MSG_LOAD_BODY_ERROR = 5;
|
|
private static final int MSG_NETWORK_ERROR = 6;
|
|
private static final int MSG_FETCHING_ATTACHMENT = 10;
|
|
private static final int MSG_VIEW_ATTACHMENT_ERROR = 12;
|
|
private static final int MSG_UPDATE_ATTACHMENT_ICON = 18;
|
|
private static final int MSG_FINISH_LOAD_ATTACHMENT = 19;
|
|
|
|
@Override
|
|
public void handleMessage(android.os.Message msg) {
|
|
switch (msg.what) {
|
|
case MSG_PROGRESS:
|
|
setProgressBarIndeterminateVisibility(msg.arg1 != 0);
|
|
break;
|
|
case MSG_ATTACHMENT_PROGRESS:
|
|
boolean progress = (msg.arg1 != 0);
|
|
if (progress) {
|
|
mProgressDialog.setMessage(
|
|
getString(R.string.message_view_fetching_attachment_progress,
|
|
mLoadAttachmentName));
|
|
mProgressDialog.show();
|
|
} else {
|
|
mProgressDialog.dismiss();
|
|
}
|
|
setProgressBarIndeterminateVisibility(progress);
|
|
break;
|
|
case MSG_LOAD_CONTENT_URI:
|
|
String uriString = (String) msg.obj;
|
|
if (mMessageContentView != null) {
|
|
mMessageContentView.loadUrl(uriString);
|
|
}
|
|
break;
|
|
case MSG_SET_ATTACHMENTS_ENABLED:
|
|
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
|
|
AttachmentInfo attachment =
|
|
(AttachmentInfo) mAttachments.getChildAt(i).getTag();
|
|
attachment.viewButton.setEnabled(msg.arg1 == 1);
|
|
attachment.downloadButton.setEnabled(msg.arg1 == 1);
|
|
}
|
|
break;
|
|
case MSG_LOAD_BODY_ERROR:
|
|
Toast.makeText(MessageView.this,
|
|
R.string.error_loading_message_body, Toast.LENGTH_LONG).show();
|
|
break;
|
|
case MSG_NETWORK_ERROR:
|
|
Toast.makeText(MessageView.this,
|
|
R.string.status_network_error, Toast.LENGTH_LONG).show();
|
|
break;
|
|
case MSG_FETCHING_ATTACHMENT:
|
|
Toast.makeText(MessageView.this,
|
|
getString(R.string.message_view_fetching_attachment_toast),
|
|
Toast.LENGTH_SHORT).show();
|
|
break;
|
|
case MSG_VIEW_ATTACHMENT_ERROR:
|
|
Toast.makeText(MessageView.this,
|
|
getString(R.string.message_view_display_attachment_toast),
|
|
Toast.LENGTH_SHORT).show();
|
|
break;
|
|
case MSG_UPDATE_ATTACHMENT_ICON:
|
|
((AttachmentInfo) mAttachments.getChildAt(msg.arg1).getTag())
|
|
.iconView.setImageBitmap((Bitmap) msg.obj);
|
|
break;
|
|
case MSG_FINISH_LOAD_ATTACHMENT:
|
|
long attachmentId = (Long)msg.obj;
|
|
doFinishLoadAttachment(attachmentId);
|
|
break;
|
|
default:
|
|
super.handleMessage(msg);
|
|
}
|
|
}
|
|
|
|
public void attachmentProgress(boolean progress) {
|
|
android.os.Message msg = android.os.Message.obtain(this, MSG_ATTACHMENT_PROGRESS);
|
|
msg.arg1 = progress ? 1 : 0;
|
|
sendMessage(msg);
|
|
}
|
|
|
|
public void progress(boolean progress) {
|
|
android.os.Message msg = android.os.Message.obtain(this, MSG_PROGRESS);
|
|
msg.arg1 = progress ? 1 : 0;
|
|
sendMessage(msg);
|
|
}
|
|
|
|
public void loadContentUri(String uriString) {
|
|
android.os.Message msg = android.os.Message.obtain(this, MSG_LOAD_CONTENT_URI);
|
|
msg.obj = uriString;
|
|
sendMessage(msg);
|
|
}
|
|
|
|
public void setAttachmentsEnabled(boolean enabled) {
|
|
android.os.Message msg = android.os.Message.obtain(this, MSG_SET_ATTACHMENTS_ENABLED);
|
|
msg.arg1 = enabled ? 1 : 0;
|
|
sendMessage(msg);
|
|
}
|
|
|
|
public void loadBodyError() {
|
|
sendEmptyMessage(MSG_LOAD_BODY_ERROR);
|
|
}
|
|
|
|
public void networkError() {
|
|
sendEmptyMessage(MSG_NETWORK_ERROR);
|
|
}
|
|
|
|
public void fetchingAttachment() {
|
|
sendEmptyMessage(MSG_FETCHING_ATTACHMENT);
|
|
}
|
|
|
|
public void attachmentViewError() {
|
|
sendEmptyMessage(MSG_VIEW_ATTACHMENT_ERROR);
|
|
}
|
|
|
|
public void updateAttachmentIcon(int pos, Bitmap icon) {
|
|
android.os.Message msg = android.os.Message.obtain(this, MSG_UPDATE_ATTACHMENT_ICON);
|
|
msg.arg1 = pos;
|
|
msg.obj = icon;
|
|
sendMessage(msg);
|
|
}
|
|
|
|
public void finishLoadAttachment(long attachmentId) {
|
|
android.os.Message msg = android.os.Message.obtain(this, MSG_FINISH_LOAD_ATTACHMENT);
|
|
msg.obj = Long.valueOf(attachmentId);
|
|
sendMessage(msg);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encapsulates known information about a single attachment.
|
|
*/
|
|
private static class AttachmentInfo {
|
|
public String name;
|
|
public String contentType;
|
|
public long size;
|
|
public long attachmentId;
|
|
public Button viewButton;
|
|
public Button downloadButton;
|
|
public ImageView iconView;
|
|
}
|
|
|
|
/**
|
|
* View a specific message found in the Email provider.
|
|
* @param messageId the message to view.
|
|
* @param mailboxId identifies the sequence of messages used for newer/older navigation.
|
|
* @param disableReplyAndForward set if reply/forward do not make sense for this message
|
|
* (e.g. messages in Trash).
|
|
*/
|
|
public static void actionView(Context context, long messageId, long mailboxId,
|
|
boolean disableReplyAndForward) {
|
|
Intent i = new Intent(context, MessageView.class);
|
|
i.putExtra(EXTRA_MESSAGE_ID, messageId);
|
|
i.putExtra(EXTRA_MAILBOX_ID, mailboxId);
|
|
i.putExtra(EXTRA_DISABLE_REPLY, disableReplyAndForward);
|
|
context.startActivity(i);
|
|
}
|
|
|
|
public static void actionView(Context context, long messageId, long mailboxId) {
|
|
actionView(context, messageId, mailboxId, false);
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(Bundle icicle) {
|
|
super.onCreate(icicle);
|
|
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);
|
|
mFavoriteIcon = (ImageView) findViewById(R.id.favorite);
|
|
mShowPicturesSection = findViewById(R.id.show_pictures_section);
|
|
mSenderPresenceView = (ImageView) findViewById(R.id.presence);
|
|
mMoveToNewer = findViewById(R.id.moveToNewer);
|
|
mMoveToOlder = findViewById(R.id.moveToOlder);
|
|
mScrollView = findViewById(R.id.scrollview);
|
|
|
|
mMoveToNewer.setOnClickListener(this);
|
|
mMoveToOlder.setOnClickListener(this);
|
|
mFromView.setOnClickListener(this);
|
|
mSenderPresenceView.setOnClickListener(this);
|
|
mFavoriteIcon.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.setVerticalScrollBarEnabled(false);
|
|
mMessageContentView.getSettings().setBlockNetworkImage(true);
|
|
mMessageContentView.getSettings().setSupportZoom(false);
|
|
mMessageContentView.setWebViewClient(new CustomWebViewClient());
|
|
|
|
mProgressDialog = new ProgressDialog(this);
|
|
mProgressDialog.setIndeterminate(true);
|
|
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
|
|
|
mDateFormat = android.text.format.DateFormat.getDateFormat(this); // short format
|
|
mTimeFormat = android.text.format.DateFormat.getTimeFormat(this); // 12/24 date format
|
|
|
|
mFavoriteIconOn = getResources().getDrawable(R.drawable.btn_star_big_buttonless_on);
|
|
mFavoriteIconOff = getResources().getDrawable(R.drawable.btn_star_big_buttonless_off);
|
|
|
|
initFromIntent();
|
|
if (icicle != null) {
|
|
mMessageId = icicle.getLong(STATE_MESSAGE_ID, mMessageId);
|
|
}
|
|
|
|
mController = Controller.getInstance(getApplication());
|
|
|
|
// This observer is used to watch for external changes to the message list
|
|
mCursorObserver = new ContentObserver(mHandler){
|
|
@Override
|
|
public void onChange(boolean selfChange) {
|
|
// get a new message list cursor, but only if we already had one
|
|
// (otherwise it's "too soon" and other pathways will cause it to be loaded)
|
|
if (mLoadMessageListTask == null && mMessageListCursor != null) {
|
|
mLoadMessageListTask = new LoadMessageListTask(mMailboxId);
|
|
mLoadMessageListTask.execute();
|
|
}
|
|
}
|
|
};
|
|
|
|
messageChanged();
|
|
}
|
|
|
|
/* package */ void initFromIntent() {
|
|
Intent intent = getIntent();
|
|
mMessageId = intent.getLongExtra(EXTRA_MESSAGE_ID, -1);
|
|
mMailboxId = intent.getLongExtra(EXTRA_MAILBOX_ID, -1);
|
|
mDisableReplyAndForward = intent.getBooleanExtra(EXTRA_DISABLE_REPLY, false);
|
|
if (mDisableReplyAndForward) {
|
|
findViewById(R.id.reply).setEnabled(false);
|
|
findViewById(R.id.reply_all).setEnabled(false);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onSaveInstanceState(Bundle state) {
|
|
super.onSaveInstanceState(state);
|
|
if (mMessageId != -1) {
|
|
state.putLong(STATE_MESSAGE_ID, mMessageId);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onResume() {
|
|
super.onResume();
|
|
mWaitForLoadMessageId = -1;
|
|
mController.addResultCallback(mControllerCallback);
|
|
if (mMessage != null) {
|
|
startPresenceCheck();
|
|
|
|
// get a new message list cursor, but only if mailbox is set
|
|
// (otherwise it's "too soon" and other pathways will cause it to be loaded)
|
|
if (mLoadMessageListTask == null && mMailboxId != -1) {
|
|
mLoadMessageListTask = new LoadMessageListTask(mMailboxId);
|
|
mLoadMessageListTask.execute();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onPause() {
|
|
super.onPause();
|
|
mController.removeResultCallback(mControllerCallback);
|
|
closeMessageListCursor();
|
|
}
|
|
|
|
private void closeMessageListCursor() {
|
|
if (mMessageListCursor != null) {
|
|
mMessageListCursor.unregisterContentObserver(mCursorObserver);
|
|
mMessageListCursor.close();
|
|
mMessageListCursor = null;
|
|
}
|
|
}
|
|
|
|
private static void cancelTask(AsyncTask<?, ?, ?> task) {
|
|
if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
|
|
task.cancel(true);
|
|
}
|
|
}
|
|
|
|
private void cancelAllTasks() {
|
|
cancelTask(mLoadMessageTask);
|
|
mLoadMessageTask = null;
|
|
cancelTask(mLoadBodyTask);
|
|
mLoadBodyTask = null;
|
|
cancelTask(mLoadAttachmentsTask);
|
|
mLoadAttachmentsTask = null;
|
|
cancelTask(mLoadMessageListTask);
|
|
mLoadMessageListTask = null;
|
|
cancelTask(mPresenceCheckTask);
|
|
mPresenceCheckTask = null;
|
|
}
|
|
|
|
/**
|
|
* We override onDestroy to make sure that the WebView gets explicitly destroyed.
|
|
* Otherwise it can leak native references.
|
|
*/
|
|
@Override
|
|
public void onDestroy() {
|
|
super.onDestroy();
|
|
cancelAllTasks();
|
|
// This is synchronized because the listener accesses mMessageContentView from its thread
|
|
synchronized (this) {
|
|
mMessageContentView.destroy();
|
|
mMessageContentView = null;
|
|
}
|
|
// the cursor was closed in onPause()
|
|
}
|
|
|
|
private void onDelete() {
|
|
if (mMessage != null) {
|
|
// the delete triggers mCursorObserver
|
|
// first move to older/newer before the actual delete
|
|
long messageIdToDelete = mMessageId;
|
|
boolean moved = moveToOlder() || moveToNewer();
|
|
mController.deleteMessage(messageIdToDelete, mMessage.mAccountKey);
|
|
Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
|
|
if (!moved) {
|
|
// this generates a benign warning "Duplicate finish request" because
|
|
// repositionMessageListCursor() will fail to reposition and do its own finish()
|
|
finish();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides for various WebView behaviors.
|
|
*/
|
|
private class CustomWebViewClient extends WebViewClient {
|
|
/**
|
|
* This is intended to mirror the operation of the original
|
|
* (see android.webkit.CallbackProxy) with one addition of intent flags
|
|
* "FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET". This improves behavior when sublaunching
|
|
* other apps via embedded URI's.
|
|
*
|
|
* We also use this hook to catch "mailto:" links and handle them locally.
|
|
*/
|
|
@Override
|
|
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
|
// hijack mailto: uri's and handle locally
|
|
if (url != null && url.toLowerCase().startsWith("mailto:")) {
|
|
return MessageCompose.actionCompose(MessageView.this, url, mAccountId);
|
|
}
|
|
|
|
// Handle most uri's via intent launch
|
|
boolean result = false;
|
|
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
|
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
|
intent.putExtra(Browser.EXTRA_APPLICATION_ID, getPackageName());
|
|
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
|
|
try {
|
|
startActivity(intent);
|
|
result = true;
|
|
} catch (ActivityNotFoundException ex) {
|
|
// If no application can handle the URL, assume that the
|
|
// caller can handle it.
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle clicks on sender, which shows {@link QuickContact} or prompts to add
|
|
* the sender as a contact.
|
|
*/
|
|
private void onClickSender() {
|
|
// Bail early if message or sender not present
|
|
if (mMessage == null) return;
|
|
|
|
final Address senderEmail = Address.unpackFirst(mMessage.mFrom);
|
|
if (senderEmail == null) return;
|
|
|
|
// First perform lookup query to find existing contact
|
|
final ContentResolver resolver = getContentResolver();
|
|
final String address = senderEmail.getAddress();
|
|
final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI,
|
|
Uri.encode(address));
|
|
final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri);
|
|
|
|
if (lookupUri != null) {
|
|
// Found matching contact, trigger QuickContact
|
|
QuickContact.showQuickContact(this, mSenderPresenceView, lookupUri,
|
|
QuickContact.MODE_LARGE, null);
|
|
} else {
|
|
// No matching contact, ask user to create one
|
|
final Uri mailUri = Uri.fromParts("mailto", address, null);
|
|
final Intent intent = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT,
|
|
mailUri);
|
|
|
|
// Pass along full E-mail string for possible create dialog
|
|
intent.putExtra(ContactsContract.Intents.EXTRA_CREATE_DESCRIPTION,
|
|
senderEmail.toString());
|
|
|
|
// Only provide personal name hint if we have one
|
|
final String senderPersonal = senderEmail.getPersonal();
|
|
if (!TextUtils.isEmpty(senderPersonal)) {
|
|
intent.putExtra(ContactsContract.Intents.Insert.NAME, senderPersonal);
|
|
}
|
|
|
|
startActivity(intent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle favorite status and write back to provider
|
|
*/
|
|
private void onClickFavorite() {
|
|
if (mMessage != null) {
|
|
// Update UI
|
|
boolean newFavorite = ! mMessage.mFlagFavorite;
|
|
mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
|
|
|
|
// Update provider
|
|
mMessage.mFlagFavorite = newFavorite;
|
|
mController.setMessageFavorite(mMessageId, newFavorite);
|
|
}
|
|
}
|
|
|
|
private void onReply() {
|
|
if (mMessage != null) {
|
|
MessageCompose.actionReply(this, mMessage.mId, false);
|
|
finish();
|
|
}
|
|
}
|
|
|
|
private void onReplyAll() {
|
|
if (mMessage != null) {
|
|
MessageCompose.actionReply(this, mMessage.mId, true);
|
|
finish();
|
|
}
|
|
}
|
|
|
|
private void onForward() {
|
|
if (mMessage != null) {
|
|
MessageCompose.actionForward(this, mMessage.mId);
|
|
finish();
|
|
}
|
|
}
|
|
|
|
private boolean moveToOlder() {
|
|
// Guard with !isLast() because Cursor.moveToNext() returns false even as it moves
|
|
// from last to after-last.
|
|
if (mMessageListCursor != null
|
|
&& !mMessageListCursor.isLast()
|
|
&& mMessageListCursor.moveToNext()) {
|
|
mMessageId = mMessageListCursor.getLong(0);
|
|
messageChanged();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private boolean moveToNewer() {
|
|
// Guard with !isFirst() because Cursor.moveToPrev() returns false even as it moves
|
|
// from first to before-first.
|
|
if (mMessageListCursor != null
|
|
&& !mMessageListCursor.isFirst()
|
|
&& mMessageListCursor.moveToPrevious()) {
|
|
mMessageId = mMessageListCursor.getLong(0);
|
|
messageChanged();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void onMarkAsRead(boolean isRead) {
|
|
if (mMessage != null && mMessage.mFlagRead != isRead) {
|
|
mMessage.mFlagRead = isRead;
|
|
mController.setMessageRead(mMessageId, isRead);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
/* package */ static 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;
|
|
}
|
|
|
|
// NOTE
|
|
// This is a placeholder for code used to accept a meeting invitation, and would presumably
|
|
// be called in response to a button press or menu selection
|
|
// The appropriate EmailServiceConstant would be changed to implement "decline" and
|
|
// "tentative" responses
|
|
private void onAccept() {
|
|
mController.sendMeetingResponse(mMessageId, EmailServiceConstants.MEETING_REQUEST_ACCEPTED,
|
|
mControllerCallback);
|
|
}
|
|
|
|
private void onDownloadAttachment(AttachmentInfo 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;
|
|
}
|
|
|
|
mLoadAttachmentId = attachment.attachmentId;
|
|
mLoadAttachmentSave = true;
|
|
mLoadAttachmentName = attachment.name;
|
|
|
|
mController.loadAttachment(attachment.attachmentId, mMessageId, mMessage.mMailboxKey,
|
|
mAccountId, mControllerCallback);
|
|
}
|
|
|
|
private void onViewAttachment(AttachmentInfo attachment) {
|
|
mLoadAttachmentId = attachment.attachmentId;
|
|
mLoadAttachmentSave = false;
|
|
mLoadAttachmentName = attachment.name;
|
|
|
|
mController.loadAttachment(attachment.attachmentId, mMessageId, mMessage.mMailboxKey,
|
|
mAccountId, mControllerCallback);
|
|
}
|
|
|
|
private void onShowPictures() {
|
|
if (mMessage != null) {
|
|
if (mMessageContentView != 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.favorite:
|
|
onClickFavorite();
|
|
break;
|
|
case R.id.reply:
|
|
onReply();
|
|
break;
|
|
case R.id.reply_all:
|
|
onReplyAll();
|
|
break;
|
|
case R.id.delete:
|
|
onDelete();
|
|
break;
|
|
case R.id.moveToOlder:
|
|
moveToOlder();
|
|
break;
|
|
case R.id.moveToNewer:
|
|
moveToNewer();
|
|
break;
|
|
case R.id.download:
|
|
onDownloadAttachment((AttachmentInfo) view.getTag());
|
|
break;
|
|
case R.id.view:
|
|
onViewAttachment((AttachmentInfo) 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:
|
|
onMarkAsRead(false);
|
|
finish();
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onCreateOptionsMenu(Menu menu) {
|
|
super.onCreateOptionsMenu(menu);
|
|
getMenuInflater().inflate(R.menu.message_view_option, menu);
|
|
if (mDisableReplyAndForward) {
|
|
menu.findItem(R.id.forward).setEnabled(false);
|
|
menu.findItem(R.id.reply).setEnabled(false);
|
|
menu.findItem(R.id.reply_all).setEnabled(false);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Re-init everything needed for changing message.
|
|
*/
|
|
private void messageChanged() {
|
|
if (Email.DEBUG) {
|
|
Email.log("MessageView: messageChanged to id=" + mMessageId);
|
|
}
|
|
cancelAllTasks();
|
|
setTitle("");
|
|
if (mMessageContentView != null) {
|
|
mMessageContentView.scrollTo(0, 0);
|
|
mMessageContentView.loadUrl("file:///android_asset/empty.html");
|
|
}
|
|
mScrollView.scrollTo(0, 0);
|
|
mAttachments.removeAllViews();
|
|
mAttachments.setVisibility(View.GONE);
|
|
mAttachmentIcon.setVisibility(View.GONE);
|
|
|
|
// Start an AsyncTask to make a new cursor and load the message
|
|
mLoadMessageTask = new LoadMessageTask(mMessageId, true);
|
|
mLoadMessageTask.execute();
|
|
updateNavigationArrows(mMessageListCursor);
|
|
}
|
|
|
|
/**
|
|
* Reposition the older/newer cursor. Finish() the activity if we are no longer
|
|
* in the list. Update the UI arrows as appropriate.
|
|
*/
|
|
private void repositionMessageListCursor() {
|
|
if (Email.DEBUG) {
|
|
Email.log("MessageView: reposition to id=" + mMessageId);
|
|
}
|
|
// position the cursor on the current message
|
|
mMessageListCursor.moveToPosition(-1);
|
|
while (mMessageListCursor.moveToNext() && mMessageListCursor.getLong(0) != mMessageId) {
|
|
}
|
|
if (mMessageListCursor.isAfterLast()) {
|
|
// overshoot - get out now, the list is no longer valid
|
|
finish();
|
|
}
|
|
updateNavigationArrows(mMessageListCursor);
|
|
}
|
|
|
|
/**
|
|
* Update the arrows based on the current position of the older/newer cursor.
|
|
*/
|
|
private void updateNavigationArrows(Cursor cursor) {
|
|
if (cursor != null) {
|
|
boolean hasNewer, hasOlder;
|
|
if (cursor.isAfterLast() || cursor.isBeforeFirst()) {
|
|
// The cursor not being on a message means that the current message was not found.
|
|
// While this should not happen, simply disable prev/next arrows in that case.
|
|
hasNewer = hasOlder = false;
|
|
} else {
|
|
hasNewer = !cursor.isFirst();
|
|
hasOlder = !cursor.isLast();
|
|
}
|
|
mMoveToNewer.setVisibility(hasNewer ? View.VISIBLE : View.INVISIBLE);
|
|
mMoveToOlder.setVisibility(hasOlder ? View.VISIBLE : View.INVISIBLE);
|
|
}
|
|
}
|
|
|
|
private Bitmap getPreviewIcon(AttachmentInfo attachment) {
|
|
try {
|
|
return BitmapFactory.decodeStream(
|
|
getContentResolver().openInputStream(
|
|
AttachmentProvider.getAttachmentThumbnailUri(
|
|
mAccountId, attachment.attachmentId,
|
|
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);
|
|
}
|
|
}
|
|
|
|
private void updateAttachmentThumbnail(long attachmentId) {
|
|
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
|
|
AttachmentInfo attachment = (AttachmentInfo) mAttachments.getChildAt(i).getTag();
|
|
if (attachment.attachmentId == attachmentId) {
|
|
Bitmap previewIcon = getPreviewIcon(attachment);
|
|
if (previewIcon != null) {
|
|
mHandler.updateAttachmentIcon(i, previewIcon);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copy data from a cursor-refreshed attachment into the UI. Called from UI thread.
|
|
*
|
|
* @param attachment A single attachment loaded from the provider
|
|
*/
|
|
private void addAttachment(Attachment attachment) {
|
|
|
|
AttachmentInfo attachmentInfo = new AttachmentInfo();
|
|
attachmentInfo.size = attachment.mSize;
|
|
attachmentInfo.contentType = attachment.mMimeType;
|
|
attachmentInfo.name = attachment.mFileName;
|
|
attachmentInfo.attachmentId = attachment.mId;
|
|
|
|
// TODO: remove this when EAS writes mime types
|
|
if (attachmentInfo.contentType == null || attachmentInfo.contentType.length() == 0) {
|
|
attachmentInfo.contentType = "application/octet-stream";
|
|
}
|
|
|
|
LayoutInflater inflater = getLayoutInflater();
|
|
View view = inflater.inflate(R.layout.message_view_attachment, null);
|
|
|
|
TextView attachmentName = (TextView)view.findViewById(R.id.attachment_name);
|
|
TextView attachmentInfoView = (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(attachmentInfo.contentType,
|
|
Email.ACCEPTABLE_ATTACHMENT_VIEW_TYPES))
|
|
|| (MimeUtility.mimeTypeMatches(attachmentInfo.contentType,
|
|
Email.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) {
|
|
attachmentView.setVisibility(View.GONE);
|
|
}
|
|
if ((!MimeUtility.mimeTypeMatches(attachmentInfo.contentType,
|
|
Email.ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))
|
|
|| (MimeUtility.mimeTypeMatches(attachmentInfo.contentType,
|
|
Email.UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))) {
|
|
attachmentDownload.setVisibility(View.GONE);
|
|
}
|
|
|
|
if (attachmentInfo.size > Email.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
|
|
attachmentView.setVisibility(View.GONE);
|
|
attachmentDownload.setVisibility(View.GONE);
|
|
}
|
|
|
|
attachmentInfo.viewButton = attachmentView;
|
|
attachmentInfo.downloadButton = attachmentDownload;
|
|
attachmentInfo.iconView = attachmentIcon;
|
|
|
|
view.setTag(attachmentInfo);
|
|
attachmentView.setOnClickListener(this);
|
|
attachmentView.setTag(attachmentInfo);
|
|
attachmentDownload.setOnClickListener(this);
|
|
attachmentDownload.setTag(attachmentInfo);
|
|
|
|
attachmentName.setText(attachmentInfo.name);
|
|
attachmentInfoView.setText(formatSize(attachmentInfo.size));
|
|
|
|
Bitmap previewIcon = getPreviewIcon(attachmentInfo);
|
|
if (previewIcon != null) {
|
|
attachmentIcon.setImageBitmap(previewIcon);
|
|
}
|
|
|
|
mAttachments.addView(view);
|
|
mAttachments.setVisibility(View.VISIBLE);
|
|
}
|
|
|
|
private class PresenceCheckTask extends AsyncTask<String, Void, Integer> {
|
|
@Override
|
|
protected Integer doInBackground(String... emails) {
|
|
Cursor cursor =
|
|
getContentResolver().query(ContactsContract.Data.CONTENT_URI,
|
|
PRESENCE_STATUS_PROJECTION, CommonDataKinds.Email.DATA + "=?", emails, null);
|
|
if (cursor != null) {
|
|
try {
|
|
if (cursor.moveToFirst()) {
|
|
int status = cursor.getInt(0);
|
|
int icon = StatusUpdates.getPresenceIconResourceId(status);
|
|
return icon;
|
|
}
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(Integer icon) {
|
|
if (icon == null) {
|
|
return;
|
|
}
|
|
updateSenderPresence(icon);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
if (mMessage != null) {
|
|
Address sender = Address.unpackFirst(mMessage.mFrom);
|
|
if (sender != null) {
|
|
String email = sender.getAddress();
|
|
if (email != null) {
|
|
mPresenceCheckTask = new PresenceCheckTask();
|
|
mPresenceCheckTask.execute(email);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
updateSenderPresence(0);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
|
|
/**
|
|
* This task finds out the messageId for the previous and next message
|
|
* in the order given by mailboxId as used in MessageList.
|
|
*
|
|
* It generates the same cursor as the one used in MessageList (but with an id-only projection),
|
|
* scans through it until finds the current messageId, and takes the previous and next ids.
|
|
*/
|
|
private class LoadMessageListTask extends AsyncTask<Void, Void, Cursor> {
|
|
private long mLocalMailboxId;
|
|
|
|
public LoadMessageListTask(long mailboxId) {
|
|
mLocalMailboxId = mailboxId;
|
|
}
|
|
|
|
@Override
|
|
protected Cursor doInBackground(Void... params) {
|
|
String selection =
|
|
Utility.buildMailboxIdSelection(getContentResolver(), mLocalMailboxId);
|
|
Cursor c = getContentResolver().query(EmailContent.Message.CONTENT_URI,
|
|
EmailContent.ID_PROJECTION,
|
|
selection, null,
|
|
EmailContent.MessageColumns.TIMESTAMP + " DESC");
|
|
return c;
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(Cursor cursor) {
|
|
if (cursor == null) {
|
|
return;
|
|
}
|
|
// remove the reference to ourselves so another one can be launched
|
|
MessageView.this.mLoadMessageListTask = null;
|
|
|
|
if (cursor.isClosed()) {
|
|
return;
|
|
}
|
|
// replace the older cursor if there is one
|
|
closeMessageListCursor();
|
|
mMessageListCursor = cursor;
|
|
mMessageListCursor.registerContentObserver(MessageView.this.mCursorObserver);
|
|
repositionMessageListCursor();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Async task for loading a single message outside of the UI thread
|
|
* Note: To support unit testing, a sentinel messageId of Long.MIN_VALUE prevents
|
|
* loading the message but leaves the activity open.
|
|
*/
|
|
private class LoadMessageTask extends AsyncTask<Void, Void, Message> {
|
|
|
|
private long mId;
|
|
private boolean mOkToFetch;
|
|
|
|
/**
|
|
* Special constructor to cache some local info
|
|
*/
|
|
public LoadMessageTask(long messageId, boolean okToFetch) {
|
|
mId = messageId;
|
|
mOkToFetch = okToFetch;
|
|
}
|
|
|
|
@Override
|
|
protected Message doInBackground(Void... params) {
|
|
if (mId == Long.MIN_VALUE) {
|
|
return null;
|
|
}
|
|
return Message.restoreMessageWithId(MessageView.this, mId);
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(Message message) {
|
|
/* doInBackground() may return null result (due to restoreMessageWithId())
|
|
* and in that situation we want to Activity.finish().
|
|
*
|
|
* OTOH we don't want to Activity.finish() for isCancelled() because this
|
|
* would introduce a surprise side-effect to task cancellation: every task
|
|
* cancelation would also result in finish().
|
|
*
|
|
* Right now LoadMesageTask is cancelled not only from onDestroy(),
|
|
* and it would be a bug to also finish() the activity in that situation.
|
|
*/
|
|
if (isCancelled()) {
|
|
return;
|
|
}
|
|
if (message == null) {
|
|
if (mId != Long.MIN_VALUE) {
|
|
finish();
|
|
}
|
|
return;
|
|
}
|
|
reloadUiFromMessage(message, mOkToFetch);
|
|
startPresenceCheck();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Async task for loading a single message body outside of the UI thread
|
|
*/
|
|
private class LoadBodyTask extends AsyncTask<Void, Void, String[]> {
|
|
|
|
private long mId;
|
|
|
|
/**
|
|
* Special constructor to cache some local info
|
|
*/
|
|
public LoadBodyTask(long messageId) {
|
|
mId = messageId;
|
|
}
|
|
|
|
@Override
|
|
protected String[] doInBackground(Void... params) {
|
|
try {
|
|
String text = null;
|
|
String html = Body.restoreBodyHtmlWithMessageId(MessageView.this, mId);
|
|
if (html == null) {
|
|
text = Body.restoreBodyTextWithMessageId(MessageView.this, mId);
|
|
}
|
|
return new String[] { text, html };
|
|
} catch (RuntimeException re) {
|
|
// This catches SQLiteException as well as other RTE's we've seen from the
|
|
// database calls, such as IllegalStateException
|
|
Log.d(Email.LOG_TAG, "Exception while loading message body: " + re.toString());
|
|
mHandler.loadBodyError();
|
|
return new String[] { null, null };
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(String[] results) {
|
|
if (results == null) {
|
|
return;
|
|
}
|
|
reloadUiFromBody(results[0], results[1]); // text, html
|
|
onMarkAsRead(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Async task for loading attachments
|
|
*
|
|
* Note: This really should only be called when the message load is complete - or, we should
|
|
* leave open a listener so the attachments can fill in as they are discovered. In either case,
|
|
* this implementation is incomplete, as it will fail to refresh properly if the message is
|
|
* partially loaded at this time.
|
|
*/
|
|
private class LoadAttachmentsTask extends AsyncTask<Long, Void, Attachment[]> {
|
|
@Override
|
|
protected Attachment[] doInBackground(Long... messageIds) {
|
|
return Attachment.restoreAttachmentsWithMessageId(MessageView.this, messageIds[0]);
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(Attachment[] attachments) {
|
|
if (attachments == null) {
|
|
return;
|
|
}
|
|
boolean htmlChanged = false;
|
|
for (Attachment attachment : attachments) {
|
|
if (mHtmlText != null && attachment.mContentId != null
|
|
&& attachment.mContentUri != null) {
|
|
// for html body, replace CID for inline images
|
|
// Regexp which matches ' src="cid:contentId"'.
|
|
String contentIdRe =
|
|
"\\s+(?i)src=\"cid(?-i):\\Q" + attachment.mContentId + "\\E\"";
|
|
String srcContentUri = " src=\"" + attachment.mContentUri + "\"";
|
|
mHtmlText = mHtmlText.replaceAll(contentIdRe, srcContentUri);
|
|
htmlChanged = true;
|
|
} else {
|
|
addAttachment(attachment);
|
|
}
|
|
}
|
|
if (htmlChanged && mMessageContentView != null) {
|
|
mMessageContentView.loadDataWithBaseURL("email://", mHtmlText, "text/html", "utf-8",
|
|
null);
|
|
}
|
|
mHtmlText = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reload the UI from a provider cursor. This must only be called from the UI thread.
|
|
*
|
|
* @param message A copy of the message loaded from the database
|
|
* @param okToFetch If true, and message is not fully loaded, it's OK to fetch from
|
|
* the network. Use false to prevent looping here.
|
|
*
|
|
* TODO: trigger presence check
|
|
*/
|
|
private void reloadUiFromMessage(Message message, boolean okToFetch) {
|
|
mMessage = message;
|
|
mAccountId = message.mAccountKey;
|
|
if (mMailboxId == -1) {
|
|
mMailboxId = message.mMailboxKey;
|
|
}
|
|
// only start LoadMessageListTask here if it's the first time
|
|
if (mMessageListCursor == null) {
|
|
mLoadMessageListTask = new LoadMessageListTask(mMailboxId);
|
|
mLoadMessageListTask.execute();
|
|
}
|
|
|
|
mSubjectView.setText(message.mSubject);
|
|
mFromView.setText(Address.toFriendly(Address.unpack(message.mFrom)));
|
|
Date date = new Date(message.mTimeStamp);
|
|
mTimeView.setText(mTimeFormat.format(date));
|
|
mDateView.setText(Utility.isDateToday(date) ? null : mDateFormat.format(date));
|
|
mToView.setText(Address.toFriendly(Address.unpack(message.mTo)));
|
|
String friendlyCc = Address.toFriendly(Address.unpack(message.mCc));
|
|
mCcView.setText(friendlyCc);
|
|
mCcContainerView.setVisibility((friendlyCc != null) ? View.VISIBLE : View.GONE);
|
|
mAttachmentIcon.setVisibility(message.mAttachments != null ? View.VISIBLE : View.GONE);
|
|
mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff);
|
|
|
|
// Handle partially-loaded email, as follows:
|
|
// 1. Check value of message.mFlagLoaded
|
|
// 2. If != LOADED, ask controller to load it
|
|
// 3. Controller callback (after loaded) should trigger LoadBodyTask & LoadAttachmentsTask
|
|
// 4. Else start the loader tasks right away (message already loaded)
|
|
if (okToFetch && message.mFlagLoaded != Message.FLAG_LOADED_COMPLETE) {
|
|
mWaitForLoadMessageId = message.mId;
|
|
mController.loadMessageForView(message.mId, mControllerCallback);
|
|
} else {
|
|
mWaitForLoadMessageId = -1;
|
|
// Ask for body
|
|
mLoadBodyTask = new LoadBodyTask(message.mId);
|
|
mLoadBodyTask.execute();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reload the body from the provider cursor. This must only be called from the UI thread.
|
|
*
|
|
* @param bodyText text part
|
|
* @param bodyHtml html part
|
|
*
|
|
* TODO deal with html vs text and many other issues
|
|
*/
|
|
private void reloadUiFromBody(String bodyText, String bodyHtml) {
|
|
String text = null;
|
|
mHtmlText = null;
|
|
boolean hasImages = false;
|
|
|
|
if (bodyHtml == null) {
|
|
text = bodyText;
|
|
/*
|
|
* Convert the plain text to HTML
|
|
*/
|
|
StringBuffer sb = new StringBuffer("<html><body>");
|
|
if (text != null) {
|
|
// Escape any inadvertent HTML in the text message
|
|
text = EmailHtmlUtil.escapeCharacterToDisplay(text);
|
|
// Find any embedded URL's and linkify
|
|
Matcher m = Patterns.WEB_URL.matcher(text);
|
|
while (m.find()) {
|
|
int start = m.start();
|
|
/*
|
|
* WEB_URL_PATTERN may match domain part of email address. To detect
|
|
* this false match, the character just before the matched string
|
|
* should not be '@'.
|
|
*/
|
|
if (start == 0 || text.charAt(start - 1) != '@') {
|
|
String url = m.group();
|
|
Matcher proto = WEB_URL_PROTOCOL.matcher(url);
|
|
String link;
|
|
if (proto.find()) {
|
|
// This is work around to force URL protocol part be lower case,
|
|
// because WebView could follow only lower case protocol link.
|
|
link = proto.group().toLowerCase() + url.substring(proto.end());
|
|
} else {
|
|
// Patterns.WEB_URL matches URL without protocol part,
|
|
// so added default protocol to link.
|
|
link = "http://" + url;
|
|
}
|
|
String href = String.format("<a href=\"%s\">%s</a>", link, url);
|
|
m.appendReplacement(sb, href);
|
|
}
|
|
else {
|
|
m.appendReplacement(sb, "$0");
|
|
}
|
|
}
|
|
m.appendTail(sb);
|
|
}
|
|
sb.append("</body></html>");
|
|
text = sb.toString();
|
|
} else {
|
|
text = bodyHtml;
|
|
mHtmlText = bodyHtml;
|
|
hasImages = IMG_TAG_START_REGEX.matcher(text).find();
|
|
}
|
|
|
|
mShowPicturesSection.setVisibility(hasImages ? View.VISIBLE : View.GONE);
|
|
if (mMessageContentView != null) {
|
|
mMessageContentView.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null);
|
|
}
|
|
|
|
// Ask for attachments after body
|
|
mLoadAttachmentsTask = new LoadAttachmentsTask();
|
|
mLoadAttachmentsTask.execute(mMessage.mId);
|
|
}
|
|
|
|
/**
|
|
* Controller results listener. This completely replaces MessagingListener
|
|
*/
|
|
class ControllerResults implements Controller.Result {
|
|
|
|
public void loadMessageForViewCallback(MessagingException result, long messageId,
|
|
int progress) {
|
|
if (messageId != MessageView.this.mMessageId
|
|
|| messageId != MessageView.this.mWaitForLoadMessageId) {
|
|
// We are not waiting for this message to load, so exit quickly
|
|
return;
|
|
}
|
|
if (result == null) {
|
|
switch (progress) {
|
|
case 0:
|
|
mHandler.progress(true);
|
|
mHandler.loadContentUri("file:///android_asset/loading.html");
|
|
break;
|
|
case 100:
|
|
mWaitForLoadMessageId = -1;
|
|
mHandler.progress(false);
|
|
// reload UI and reload everything else too
|
|
// pass false to LoadMessageTask to prevent looping here
|
|
cancelAllTasks();
|
|
mLoadMessageTask = new LoadMessageTask(mMessageId, false);
|
|
mLoadMessageTask.execute();
|
|
break;
|
|
default:
|
|
// do nothing - we don't have a progress bar at this time
|
|
break;
|
|
}
|
|
} else {
|
|
mWaitForLoadMessageId = -1;
|
|
mHandler.progress(false);
|
|
mHandler.networkError();
|
|
mHandler.loadContentUri("file:///android_asset/empty.html");
|
|
}
|
|
}
|
|
|
|
public void loadAttachmentCallback(MessagingException result, long messageId,
|
|
long attachmentId, int progress) {
|
|
if (messageId == MessageView.this.mMessageId) {
|
|
if (result == null) {
|
|
switch (progress) {
|
|
case 0:
|
|
mHandler.setAttachmentsEnabled(false);
|
|
mHandler.attachmentProgress(true);
|
|
mHandler.fetchingAttachment();
|
|
break;
|
|
case 100:
|
|
mHandler.setAttachmentsEnabled(true);
|
|
mHandler.attachmentProgress(false);
|
|
updateAttachmentThumbnail(attachmentId);
|
|
mHandler.finishLoadAttachment(attachmentId);
|
|
break;
|
|
default:
|
|
// do nothing - we don't have a progress bar at this time
|
|
break;
|
|
}
|
|
} else {
|
|
mHandler.setAttachmentsEnabled(true);
|
|
mHandler.attachmentProgress(false);
|
|
mHandler.networkError();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void updateMailboxCallback(MessagingException result, long accountId,
|
|
long mailboxId, int progress, int numNewMessages) {
|
|
if (result != null || progress == 100) {
|
|
Email.updateMailboxRefreshTime(mailboxId);
|
|
}
|
|
}
|
|
|
|
public void updateMailboxListCallback(MessagingException result, long accountId,
|
|
int progress) {
|
|
}
|
|
|
|
public void serviceCheckMailCallback(MessagingException result, long accountId,
|
|
long mailboxId, int progress, long tag) {
|
|
}
|
|
|
|
public void sendMailCallback(MessagingException result, long accountId, long messageId,
|
|
int progress) {
|
|
}
|
|
}
|
|
|
|
|
|
// @Override
|
|
// public void loadMessageForViewBodyAvailable(Account account, String folder,
|
|
// String uid, com.android.email.mail.Message message) {
|
|
// MessageView.this.mOldMessage = message;
|
|
// try {
|
|
// Part part = MimeUtility.findFirstPartByMimeType(mOldMessage, "text/html");
|
|
// if (part == null) {
|
|
// part = MimeUtility.findFirstPartByMimeType(mOldMessage, "text/plain");
|
|
// }
|
|
// if (part != null) {
|
|
// String text = MimeUtility.getTextFromPart(part);
|
|
// if (part.getMimeType().equalsIgnoreCase("text/html")) {
|
|
// text = EmailHtmlUtil.resolveInlineImage(
|
|
// getContentResolver(), mAccount.mId, text, mOldMessage, 0);
|
|
// } else {
|
|
// // And also escape special character, such as "<>&",
|
|
// // to HTML escape sequence.
|
|
// text = EmailHtmlUtil.escapeCharacterToDisplay(text);
|
|
|
|
// /*
|
|
// * Linkify the plain text and convert it to HTML by replacing
|
|
// * \r?\n with <br> and adding a html/body wrapper.
|
|
// */
|
|
// StringBuffer sb = new StringBuffer("<html><body>");
|
|
// if (text != null) {
|
|
// Matcher m = Patterns.WEB_URL.matcher(text);
|
|
// while (m.find()) {
|
|
// int start = m.start();
|
|
// /*
|
|
// * WEB_URL_PATTERN may match domain part of email address. To detect
|
|
// * this false match, the character just before the matched string
|
|
// * should not be '@'.
|
|
// */
|
|
// if (start == 0 || text.charAt(start - 1) != '@') {
|
|
// String url = m.group();
|
|
// Matcher proto = WEB_URL_PROTOCOL.matcher(url);
|
|
// String link;
|
|
// if (proto.find()) {
|
|
// // Work around to force URL protocol part be lower case,
|
|
// // since WebView could follow only lower case protocol link.
|
|
// link = proto.group().toLowerCase()
|
|
// + url.substring(proto.end());
|
|
// } else {
|
|
// // Patterns.WEB_URL matches URL without protocol part,
|
|
// // so added default protocol to link.
|
|
// link = "http://" + url;
|
|
// }
|
|
// String href = String.format("<a href=\"%s\">%s</a>", link, url);
|
|
// m.appendReplacement(sb, href);
|
|
// }
|
|
// else {
|
|
// m.appendReplacement(sb, "$0");
|
|
// }
|
|
// }
|
|
// m.appendTail(sb);
|
|
// }
|
|
// sb.append("</body></html>");
|
|
// text = sb.toString();
|
|
// }
|
|
|
|
// /*
|
|
// * 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).find()) {
|
|
// mHandler.showShowPictures(true);
|
|
// }
|
|
|
|
// loadMessageContentText(text);
|
|
// }
|
|
// else {
|
|
// loadMessageContentUrl("file:///android_asset/empty.html");
|
|
// }
|
|
// // renderAttachments(mOldMessage, 0);
|
|
// }
|
|
// catch (Exception e) {
|
|
// if (Email.LOGD) {
|
|
// Log.v(Email.LOG_TAG, "loadMessageForViewBodyAvailable", e);
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
/**
|
|
* Back in the UI thread, handle the final steps of downloading an attachment (view or save).
|
|
*
|
|
* @param attachmentId the attachment that was just downloaded
|
|
*/
|
|
private void doFinishLoadAttachment(long attachmentId) {
|
|
// If the result does't line up, just skip it - we handle one at a time.
|
|
if (attachmentId != mLoadAttachmentId) {
|
|
return;
|
|
}
|
|
Attachment attachment =
|
|
Attachment.restoreAttachmentWithId(MessageView.this, attachmentId);
|
|
Uri attachmentUri = AttachmentProvider.getAttachmentUri(mAccountId, attachment.mId);
|
|
Uri contentUri =
|
|
AttachmentProvider.resolveAttachmentIdToContentUri(getContentResolver(), attachmentUri);
|
|
|
|
if (mLoadAttachmentSave) {
|
|
try {
|
|
File file = createUniqueFile(Environment.getExternalStorageDirectory(),
|
|
attachment.mFileName);
|
|
InputStream in = getContentResolver().openInputStream(contentUri);
|
|
OutputStream out = new FileOutputStream(file);
|
|
IOUtils.copy(in, out);
|
|
out.flush();
|
|
out.close();
|
|
in.close();
|
|
|
|
Toast.makeText(MessageView.this, String.format(
|
|
getString(R.string.message_view_status_attachment_saved), file.getName()),
|
|
Toast.LENGTH_LONG).show();
|
|
|
|
new MediaScannerNotifier(this, file, mHandler);
|
|
} catch (IOException ioe) {
|
|
Toast.makeText(MessageView.this,
|
|
getString(R.string.message_view_status_attachment_not_saved),
|
|
Toast.LENGTH_LONG).show();
|
|
}
|
|
} else {
|
|
try {
|
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
intent.setData(contentUri);
|
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
| Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
|
|
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.
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
}
|
|
}
|