From b5903285dd6d2d95ce80016fa192c9ed44a04f25 Mon Sep 17 00:00:00 2001 From: Andrew Stadler Date: Mon, 15 Jun 2009 16:46:38 -0700 Subject: [PATCH] Enable MessageView using provider. Very primitive, just shows a few headers, and no navigation at all (you can only go "back" from here. --- .../email/activity/FolderMessageList.java | 67 +++- .../android/email/activity/MessageView.java | 290 +++++++++++++++--- .../android/email/provider/EmailContent.java | 4 +- 3 files changed, 312 insertions(+), 49 deletions(-) diff --git a/src/com/android/email/activity/FolderMessageList.java b/src/com/android/email/activity/FolderMessageList.java index 9159184d9..3f9d1a94b 100644 --- a/src/com/android/email/activity/FolderMessageList.java +++ b/src/com/android/email/activity/FolderMessageList.java @@ -550,7 +550,8 @@ public class FolderMessageList extends ExpandableListActivity { */ mMessagingListener = new NewMessagingListener(); - mLoadMailboxesTask = (LoadMailBoxesTask) new LoadMailBoxesTask().execute(); + mLoadMailboxesTask = (LoadMailBoxesTask) new LoadMailBoxesTask(savedInstanceState); + mLoadMailboxesTask.execute(); setTitle(mAccount.getDescription()); } @@ -681,7 +682,7 @@ public class FolderMessageList extends ExpandableListActivity { int childPosition, long id) { // TODO completely rewrite this. For now, quick access to new onOpenMessage call. - + onOpenMessage(groupPosition, childPosition); return true; // "handled" /* @@ -750,6 +751,7 @@ public class FolderMessageList extends ExpandableListActivity { */ } + @Deprecated private void onOpenMessage(FolderInfoHolder folder, MessageInfoHolder message) { /* * We set read=true here for UI performance reasons. The actual value will get picked up @@ -774,6 +776,30 @@ public class FolderMessageList extends ExpandableListActivity { MessageView.actionView(this, mAccountId, folder.name, message.uid, folderUids); } } + + /** + * TODO we can probably do the cursor work and message ID lookup in the caller, especially + * when we implement other handlers like onReply, onDelete, etc. + * + * @param groupPosition + * @param childPosition + */ + private void onOpenMessage(int groupPosition, int childPosition) { + Cursor mailboxCursor = mNewAdapter.getGroup(groupPosition); + Cursor messageCursor = mNewAdapter.getChild(groupPosition, childPosition); + + int mailboxTypeColumn = mailboxCursor.getColumnIndex(EmailContent.MailboxColumns.TYPE); + int mailboxType = mailboxCursor.getInt(mailboxTypeColumn); + if (mailboxType == EmailContent.Mailbox.TYPE_DRAFTS) { + // TODO - compose from draft + } else { + // TODO - save enough data to recreate the cursor, to enable prev/next in MessageView + int messageIdColumn = messageCursor.getColumnIndex(EmailContent.RECORD_ID); + long messageId = messageCursor.getLong(messageIdColumn); + MessageView.actionView(this, messageId); + } + + } private void onEditAccount() { // We request a remote refresh *after* the account settings because user changes may @@ -859,6 +885,11 @@ public class FolderMessageList extends ExpandableListActivity { int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition); if (childPosition < mAdapter.getChildrenCount(groupPosition)) { + switch (item.getItemId()) { + case R.id.open: + onOpenMessage(groupPosition, childPosition); + break; + } // TODO: completely rewrite this. For now, just don't crash. /* @@ -922,6 +953,15 @@ public class FolderMessageList extends ExpandableListActivity { * Async task to handle the cursor query out of the UI thread */ private class LoadMailBoxesTask extends AsyncTask { + + // We park the activity's saved instance state here, to apply + // after the cursor is loaded and the adapter is ready for UI + Bundle mSavedInstanceState; + + public LoadMailBoxesTask(Bundle savedInstanceState) { + super(); + mSavedInstanceState = savedInstanceState; + } @Override protected Cursor doInBackground(Void... params) { @@ -943,6 +983,14 @@ public class FolderMessageList extends ExpandableListActivity { new NewFolderMessageListAdapter(cursor, FolderMessageList.this); mListView.setAdapter(FolderMessageList.this.mNewAdapter); + // After setting up the adapter & data, restore its state (if applicable) + if (mSavedInstanceState != null) { + mRestoringState = true; + onRestoreListState(mSavedInstanceState); + mRestoringState = false; + mRefreshRemote |= mSavedInstanceState.getBoolean(STATE_KEY_REFRESH_REMOTE); + } + /** * For debugging purposes, add a single mailbox when there are none */ @@ -1008,7 +1056,8 @@ public class FolderMessageList extends ExpandableListActivity { // msg.mFlagFavorite = false; // msg.mFlagAttachment = false; // msg.mFlags = 0; - // msg.mTextInfo; +// msg.mText = "This is the body text of the 1st email."; +// msg.mTextInfo = "X;X;8;" + msg.mText.length()*2; // msg.mHtmlInfo; // msg.mServerId; // msg.mServerIntId; @@ -1034,6 +1083,8 @@ public class FolderMessageList extends ExpandableListActivity { msg.mMailboxKey = mailbox1Id; msg.mAccountKey = FolderMessageList.this.mAccountId; msg.mFrom = "sender2@google.com"; +// msg.mText = "This is the body text of this email."; +// msg.mTextInfo = "X;X;8;" + msg.mText.length()*2; msg.save(FolderMessageList.this); msg = new EmailContent.Message(); @@ -1043,6 +1094,8 @@ public class FolderMessageList extends ExpandableListActivity { msg.mMailboxKey = mailbox1Id; msg.mAccountKey = FolderMessageList.this.mAccountId; msg.mFrom = "sender3@google.com"; +// msg.mText = "This is the body text of the 3rd email."; +// msg.mTextInfo = "X;X;8;" + msg.mText.length()*2; msg.save(FolderMessageList.this); msg = new EmailContent.Message(); @@ -1052,6 +1105,8 @@ public class FolderMessageList extends ExpandableListActivity { msg.mMailboxKey = mailbox2Id; msg.mAccountKey = FolderMessageList.this.mAccountId; msg.mFrom = "sender4@google.com"; +// msg.mText = "This is the body text of the 4th email."; +// msg.mTextInfo = "X;X;8;" + msg.mText.length()*2; msg.save(FolderMessageList.this); } } @@ -1062,10 +1117,10 @@ public class FolderMessageList extends ExpandableListActivity { * Async task for loading a single folder out of the UI thread */ private class LoadMessagesTask extends AsyncTask { - + private int mGroupNumber; private long mMailboxKey; - + /** * Special constructor to cache some local info */ @@ -1089,7 +1144,7 @@ public class FolderMessageList extends ExpandableListActivity { }, null); } - + @Override protected void onPostExecute(Cursor cursor) { if (DBG_LOG_CURSORS) { diff --git a/src/com/android/email/activity/MessageView.java b/src/com/android/email/activity/MessageView.java index 858e5c771..ae2b4471c 100644 --- a/src/com/android/email/activity/MessageView.java +++ b/src/com/android/email/activity/MessageView.java @@ -47,10 +47,10 @@ import android.graphics.BitmapFactory; 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.os.Process; import android.provider.Contacts; import android.provider.Contacts.Intents; import android.provider.Contacts.People; @@ -83,6 +83,7 @@ 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_ACCOUNT_ID = "com.android.email.MessageView_account_id"; private static final String EXTRA_FOLDER = "com.android.email.MessageView_folder"; private static final String EXTRA_MESSAGE = "com.android.email.MessageView_message"; @@ -99,7 +100,17 @@ public class MessageView extends Activity 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[] { + EmailContent.RECORD_ID, EmailContent.BodyColumns.MESSAGE_KEY, + EmailContent.BodyColumns.HTML_CONTENT, EmailContent.BodyColumns.TEXT_CONTENT + }; + 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; @@ -116,13 +127,16 @@ public class MessageView extends Activity private long mAccountId; private EmailContent.Account mAccount; + private long mMessageId; + + private LoadMessageTask mLoadMessageTask; + private LoadBodyTask mLoadBodyTask; + private String mFolder; private String mMessageUid; - private ArrayList mFolderUids; + private Cursor mMessageListCursor; private Message mMessage; - private String mNextMessageUid = null; - private String mPreviousMessageUid = null; private java.text.DateFormat mDateFormat; private java.text.DateFormat mTimeFormat; @@ -301,11 +315,24 @@ public class MessageView extends Activity public ImageView iconView; } + /** + * View a specific message found in the Email provider. + * + * TODO: Find a way to pass a cursor so we can iterator prev/next as well + */ + public static void actionView(Context context, long messageId) { + Intent i = new Intent(context, MessageView.class); + i.putExtra(EXTRA_MESSAGE_ID, messageId); + context.startActivity(i); + } + + @Deprecated public static void actionView(Context context, long accountId, String folder, String messageUid, ArrayList folderUids) { actionView(context, accountId, folder, messageUid, folderUids, null); } + @Deprecated public static void actionView(Context context, long accountId, String folder, String messageUid, ArrayList folderUids, Bundle extras) { Intent i = new Intent(context, MessageView.class); @@ -363,11 +390,12 @@ public class MessageView extends Activity mTimeFormat = android.text.format.DateFormat.getTimeFormat(this); // 12/24 date format Intent intent = getIntent(); - mAccountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1); - mAccount = EmailContent.Account.restoreAccountWithId(this, mAccountId); - mFolder = intent.getStringExtra(EXTRA_FOLDER); - mMessageUid = intent.getStringExtra(EXTRA_MESSAGE); - mFolderUids = intent.getStringArrayListExtra(EXTRA_FOLDER_UIDS); + mMessageId = intent.getLongExtra(EXTRA_MESSAGE_ID, -1); +// mAccountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1); +// mAccount = EmailStore.Account.restoreAccountWithId(this, mAccountId); +// mFolder = intent.getStringExtra(EXTRA_FOLDER); +// mMessageUid = intent.getStringExtra(EXTRA_MESSAGE); + mMessageListCursor = null; // TODO - pass message list cursor so we can prev/next View next = findViewById(R.id.next); View previous = findViewById(R.id.previous); @@ -375,7 +403,11 @@ public class MessageView extends Activity * Next and Previous Message are not shown in landscape mode, so * we need to check before we use them. */ - if (next != null && previous != null) { + if (next != null && previous != null && mMessageListCursor != null) { + // TODO analyze based on cursor, not on a big nasty array + next.setVisibility(View.GONE); + previous.setVisibility(View.GONE); +/* next.setOnClickListener(this); previous.setOnClickListener(this); @@ -388,8 +420,10 @@ public class MessageView extends Activity if (goNext) { next.requestFocus(); } +*/ } +/* MessagingController.getInstance(getApplication()).addListener(mListener); new Thread() { @Override @@ -405,22 +439,10 @@ public class MessageView extends Activity 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; - } - } +*/ + // Start an AsyncTask to make a new cursor and load the message + mLoadMessageTask = new LoadMessageTask(mMessageId); + mLoadMessageTask.execute(); } @Override @@ -445,6 +467,18 @@ public class MessageView extends Activity @Override public void onDestroy() { super.onDestroy(); + + if (mLoadMessageTask != null && + mLoadMessageTask.getStatus() != AsyncTask.Status.FINISHED) { + mLoadMessageTask.cancel(true); + mLoadMessageTask = null; + } + if (mLoadBodyTask != null && + mLoadBodyTask.getStatus() != AsyncTask.Status.FINISHED) { + mLoadBodyTask.cancel(true); + mLoadBodyTask = null; + } + // This is synchronized because the listener accesses mMessageContentView from its thread synchronized (this) { mMessageContentView.destroy(); @@ -461,19 +495,13 @@ public class MessageView extends Activity 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(); + if (onPrevious()) { + return; } + if (onNext()) { + return; + } + finish(); } } @@ -526,16 +554,24 @@ public class MessageView extends Activity } } - private void onNext() { + private boolean onNext() { + // TODO make this work using a cursor + return false; +/* Bundle extras = new Bundle(1); extras.putBoolean(EXTRA_NEXT, true); MessageView.actionView(this, mAccountId, mFolder, mNextMessageUid, mFolderUids, extras); finish(); +*/ } - private void onPrevious() { + private boolean onPrevious() { + // TODO make this work using a cursor + return false; +/* MessageView.actionView(this, mAccountId, mFolder, mPreviousMessageUid, mFolderUids); finish(); +*/ } private void onMarkAsUnread() { @@ -874,6 +910,178 @@ public class MessageView extends Activity } mSenderPresenceView.setImageResource(presenceIconId); } + + /** + * Async task for loading a single message outside of the UI thread + * + * TODO: start and stop progress indicator + * TODO: set up to listen for additional updates (cursor data changes) + */ + private class LoadMessageTask extends AsyncTask { + + private long mMessageId; + + /** + * Special constructor to cache some local info + */ + public LoadMessageTask(long messageId) { + mMessageId = messageId; + } + + @Override + protected Cursor doInBackground(Void... params) { + return MessageView.this.managedQuery( + EmailContent.Message.CONTENT_URI, + EmailContent.Message.CONTENT_PROJECTION, + EmailContent.RECORD_ID + "=?", + new String[] { + String.valueOf(mMessageId) + }, + null); + } + + @Override + protected void onPostExecute(Cursor cursor) { + if (cursor.moveToFirst()) { + reloadUiFromCursor(cursor); + } else { + // toast? why would this fail? + } + } + } + + /** + * Async task for loading a single message body outside of the UI thread + * + * TODO: smarter loading of html vs. text + */ + private class LoadBodyTask extends AsyncTask { + + private long mBodyId; + + /** + * Special constructor to cache some local info + */ + public LoadBodyTask(long bodyId) { + mBodyId = bodyId; + } + + @Override + protected Cursor doInBackground(Void... params) { + return MessageView.this.managedQuery( + EmailContent.Body.CONTENT_URI, + BODY_CONTENT_PROJECTION, + EmailContent.RECORD_ID + "=?", + new String[] { + String.valueOf(mBodyId) + }, + null); + } + + @Override + protected void onPostExecute(Cursor cursor) { + if (cursor.moveToFirst()) { + reloadBodyFromCursor(cursor); + } else { + // toast? why would this fail? + reloadBodyFromCursor(null); // hack to force text for display + } + } + } + + /** + * Reload the UI from a provider cursor. This must only be called from the UI thread. + * + * @param cursor A cursor loaded from EmailStore.Message. + * + * TODO: field-specific formatting rules + * TODO: timestamp -> date/time + * TODO: body + * TODO: attachments + * TODO: trigger presence check + * TODO: trigger body load + */ + private void reloadUiFromCursor(Cursor cursor) { + EmailContent.Message message = EmailContent.getContent(cursor, EmailContent.Message.class); + + mSubjectView.setText(message.mSubject); + mFromView.setText(message.mFrom); + mTimeView.setText(String.valueOf(message.mTimeStamp)); + mDateView.setText(String.valueOf(message.mTimeStamp)); + mToView.setText(message.mTo); + mCcView.setText(message.mCc); + mCcContainerView.setVisibility((message.mCc != null) ? View.VISIBLE : View.GONE); + mAttachmentIcon.setVisibility(message.mAttachments != null ? View.VISIBLE : View.GONE); + + // Ask for body + mLoadBodyTask = new LoadBodyTask(message.mBodyKey); + mLoadBodyTask.execute(); + } + + /** + * Reload the body from the provider cursor. This must only be called from the UI thread. + * + * @param cursor + * + * TODO deal with html vs text and many other issues + */ + private void reloadBodyFromCursor(Cursor cursor) { + // TODO Remove this hack that forces some text to test the code + String text; + if (cursor == null) { + text = "This is dummy text from MessageView.reloadBodyFromCursor()"; + } else { + text = cursor.getString(BODY_CONTENT_COLUMN_TEXT_CONTENT); + } + + // This code is stolen from Listener.loadMessageForViewBodyAvailable + // 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
and adding a html/body wrapper. + */ + StringBuffer sb = new StringBuffer(""); + if (text != null) { + Matcher m = Regex.WEB_URL_PATTERN.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 { + // Regex.WEB_URL_PATTERN matches URL without protocol part, + // so added default protocol to link. + link = "http://" + url; + } + String href = String.format("%s", link, url); + m.appendReplacement(sb, href); + } + else { + m.appendReplacement(sb, "$0"); + } + } + m.appendTail(sb); + } + sb.append(""); + text = sb.toString(); + + if (mMessageContentView != null) { + mMessageContentView.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null); + } + } class Listener extends MessagingListener { @Override diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java index eb0fc03b5..64c4de409 100644 --- a/src/com/android/email/provider/EmailContent.java +++ b/src/com/android/email/provider/EmailContent.java @@ -634,10 +634,10 @@ public abstract class EmailContent { Body body = new Body(); ContentValues cv = new ContentValues(); if (mText != null) { - cv.put("text", mText); + cv.put(Body.TEXT_CONTENT, mText); } if (mHtml != null) { - cv.put("html", mHtml); + cv.put(Body.HTML_CONTENT, mHtml); } b = getSaveOrUpdateBuilder(doSave, Body.CONTENT_URI, 0);