Implement SmartReply/SmartForward for EAS; fixes #2098779

* SmartForward and SmartReply are EAS commands that automatically
  include the original message and, if a forward, all original
  attachments, regardless of whether they've been downloaded to
  the device
* Both commands improve battery life by sending less data; greatly
  so for SmartForward if there are attachments

Change-Id: I12432cd5275a3b54e9a80d5cd59da437c4a086cc
This commit is contained in:
Marc Blank 2009-09-08 16:44:00 -07:00
parent 42c130dcf1
commit 1d98989222
6 changed files with 160 additions and 28 deletions

View File

@ -59,11 +59,15 @@ public class Rfc822Output {
static final SimpleDateFormat mDateFormat =
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
/*package*/ static String buildBodyText(Context context, Message message) {
int flags = message.mFlags;
/*package*/ static String buildBodyText(Context context, Message message,
boolean appendQuotedText) {
Body body = Body.restoreBodyWithMessageId(context, message.mId);
String text = body.mTextContent;
if (!appendQuotedText) {
return text;
}
String quotedText = body.mTextReply;
if (quotedText != null) {
// fix CR-LF line endings to LF-only needed by EditText.
@ -71,6 +75,7 @@ public class Rfc822Output {
quotedText = matcher.replaceAll("\n");
}
String fromAsString = Address.unpackToString(message.mFrom);
int flags = message.mFlags;
if ((flags & Message.FLAG_TYPE_REPLY) != 0) {
text += context.getString(R.string.message_compose_reply_header_fmt, fromAsString);
if (quotedText != null) {
@ -97,12 +102,12 @@ public class Rfc822Output {
* @param context system context for accessing the provider
* @param messageId the message to write out
* @param out the output stream to write the message to
* @param appendQuotedText whether or not to append quoted text if this is a reply/forward
*
* TODO is there anything in the flags fields we need to look at?
* TODO alternative parts (e.g. text+html) are not supported here.
*/
public static void writeTo(Context context, long messageId, OutputStream out)
throws IOException, MessagingException {
public static void writeTo(Context context, long messageId, OutputStream out,
boolean appendQuotedText) throws IOException, MessagingException {
Message message = Message.restoreMessageWithId(context, messageId);
if (message == null) {
// throw something?
@ -129,7 +134,7 @@ public class Rfc822Output {
writeAddressHeader(writer, "Reply-To", message.mReplyTo);
// Analyze message and determine if we have multiparts
String text = buildBodyText(context, message);
String text = buildBodyText(context, message, appendQuotedText);
Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
Cursor attachmentsCursor = context.getContentResolver().query(uri,

View File

@ -232,7 +232,7 @@ public class SmtpSender extends Sender {
executeSimpleCommand("DATA");
// TODO byte stuffing
Rfc822Output.writeTo(mContext, messageId,
new EOLConvertingOutputStream(mTransport.getOutputStream()));
new EOLConvertingOutputStream(mTransport.getOutputStream()), true);
executeSimpleCommand("\r\n.");
} catch (IOException ioe) {
throw new MessagingException("Unable to send message", ioe);

View File

@ -22,9 +22,13 @@ import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.exchange.utility.FileLogger;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.NetworkInfo.DetailedState;
import android.util.Log;
@ -325,4 +329,37 @@ public abstract class AbstractSyncService implements Runnable {
}
return null;
}
/**
* Convenience method wrapping calls to retrieve columns from a single row, via EmailProvider.
* The arguments are exactly the same as to contentResolver.query(). Results are returned in
* an array of Strings corresponding to the columns in the projection.
*/
protected String[] getRowColumns(Uri contentUri, String[] projection, String selection,
String[] selectionArgs) {
String[] values = new String[projection.length];
ContentResolver cr = mContext.getContentResolver();
Cursor c = cr.query(contentUri, projection, selection, selectionArgs, null);
try {
if (c.moveToFirst()) {
for (int i = 0; i < projection.length; i++) {
values[i] = c.getString(i);
}
} else {
return null;
}
} finally {
c.close();
}
return values;
}
/**
* Convenience method for retrieving columns from a particular row in EmailProvider.
* Passed in here are a base uri (e.g. Message.CONTENT_URI), the unique id of a row, and
* a projection. This method calls the previous one with the appropriate URI.
*/
protected String[] getRowColumns(Uri baseUri, long id, String ... projection) {
return getRowColumns(ContentUris.withAppendedId(baseUri, id), projection, null, null);
}
}

View File

@ -19,7 +19,10 @@ package com.android.exchange;
import com.android.email.mail.MessagingException;
import com.android.email.mail.transport.Rfc822Output;
import com.android.email.provider.EmailContent.Body;
import com.android.email.provider.EmailContent.BodyColumns;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.Message;
import com.android.email.provider.EmailContent.MessageColumns;
import com.android.email.provider.EmailContent.SyncColumns;
@ -38,16 +41,19 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
public class EasOutboxService extends EasSyncService {
public static final int SEND_FAILED = 1;
public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
MessageColumns.MAILBOX_KEY + "=? and " + SyncColumns.SERVER_ID + "!=" + SEND_FAILED;
public static final String[] BODY_SOURCE_PROJECTION =
new String[] {BodyColumns.SOURCE_MESSAGE_KEY};
public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
public EasOutboxService(Context _context, Mailbox _mailbox) {
super(_context, _mailbox);
mContext = _context;
}
private void sendCallback(long msgId, String subject, int status) {
@ -73,34 +79,70 @@ public class EasOutboxService extends EasSyncService {
File tmpFile = File.createTempFile("eas_", "tmp", cacheDir);
// Write the output to a temporary file
try {
String[] cols = getRowColumns(Message.CONTENT_URI, msgId, MessageColumns.FLAGS,
MessageColumns.SUBJECT);
int flags = Integer.parseInt(cols[0]);
String subject = cols[1];
boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
// The reference message and mailbox are called item and collection in EAS
String itemId = null;
String collectionId = null;
if (reply || forward) {
// First, we need to get the id of the reply/forward message
cols = getRowColumns(Body.CONTENT_URI, BODY_SOURCE_PROJECTION,
WHERE_MESSAGE_KEY, new String[] {Long.toString(msgId)});
if (cols != null) {
long refId = Long.parseLong(cols[0]);
// Then, we need the serverId and mailboxKey of the message
cols = getRowColumns(Message.CONTENT_URI, refId, SyncColumns.SERVER_ID,
MessageColumns.MAILBOX_KEY);
if (cols != null) {
itemId = cols[0];
long boxId = Long.parseLong(cols[1]);
// Then, we need the serverId of the mailbox
cols = getRowColumns(Mailbox.CONTENT_URI, boxId, MailboxColumns.SERVER_ID);
if (cols != null) {
collectionId = cols[0];
}
}
}
}
boolean smartSend = itemId != null && collectionId != null;
// Write the message in rfc822 format to the temporary file
FileOutputStream fileStream = new FileOutputStream(tmpFile);
Rfc822Output.writeTo(mContext, msgId, fileStream);
Rfc822Output.writeTo(mContext, msgId, fileStream, !smartSend);
fileStream.close();
// Now, get an input stream to our new file and create an entity with it
// Now, get an input stream to our temporary file and create an entity with it
FileInputStream inputStream = new FileInputStream(tmpFile);
InputStreamEntity inputEntity =
new InputStreamEntity(inputStream, tmpFile.length());
// Send the post to the server
HttpResponse resp =
sendHttpClientPost("SendMail&SaveInSent=T", inputEntity);
// Create the appropriate command and POST it to the server
String cmd = "SendMail&SaveInSent=T";
if (smartSend) {
cmd = reply ? "SmartReply" : "SmartForward";
cmd += "&ItemId=" + URLEncoder.encode(itemId, "UTF-8") + "&CollectionId="
+ URLEncoder.encode(collectionId, "UTF-8") + "&SaveInSent=T";
}
HttpResponse resp = sendHttpClientPost(cmd, inputEntity);
inputStream.close();
int code = resp.getStatusLine().getStatusCode();
if (code == HttpStatus.SC_OK) {
userLog("Deleting message...");
// Yes it would be marginally faster to get only the subject, but it would also
// be more code; note, we need the subject for the callback below, since the
// message gets deleted just below. This allows the UI to present the subject
// of the message in a Toast or other notification
Message msg = Message.restoreMessageWithId(mContext, msgId);
mContext.getContentResolver().delete(ContentUris.withAppendedId(
Message.CONTENT_URI, msgId), null, null);
mContentResolver.delete(ContentUris.withAppendedId(Message.CONTENT_URI, msgId),
null, null);
result = EmailServiceStatus.SUCCESS;
sendCallback(-1, msg.mSubject, EmailServiceStatus.SUCCESS);
sendCallback(-1, subject, EmailServiceStatus.SUCCESS);
} else {
ContentValues cv = new ContentValues();
cv.put(SyncColumns.SERVER_ID, SEND_FAILED);
Message.update(mContext, Message.CONTENT_URI, msgId, cv);
// TODO REMOTE_EXCEPTION is temporary; add better error codes
result = EmailServiceStatus.REMOTE_EXCEPTION;
if (isAuthError(code)) {
result = EmailServiceStatus.LOGIN_FAILED;
@ -127,6 +169,7 @@ public class EasOutboxService extends EasSyncService {
File cacheDir = mContext.getCacheDir();
try {
mDeviceId = SyncManager.getDeviceId();
Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
Message.ID_COLUMN_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
new String[] {Long.toString(mMailbox.mId)}, null);
@ -150,6 +193,8 @@ public class EasOutboxService extends EasSyncService {
c.close();
}
mExitStatus = EXIT_DONE;
} catch (IOException e) {
mExitStatus = EXIT_IO_ERROR;
} catch (Exception e) {
mExitStatus = EXIT_EXCEPTION;
} finally {

View File

@ -123,7 +123,7 @@ public class EasSyncService extends AbstractSyncService {
// Reasonable default
String mProtocolVersion = "2.5";
public Double mProtocolVersionDouble;
private String mDeviceId = null;
protected String mDeviceId = null;
private String mDeviceType = "Android";
private String mAuthString = null;
private String mCmdString = null;
@ -417,9 +417,22 @@ public class EasSyncService extends AbstractSyncService {
protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
throws IOException {
HttpClient client = getHttpClient(timeout);
String us = makeUriString(cmd, null);
// Split the mail sending commands
String extra = null;
boolean msg = false;
if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) {
int cmdLength = cmd.length() - 1;
extra = cmd.substring(cmdLength);
cmd = cmd.substring(0, cmdLength);
msg = true;
} else if (cmd.startsWith("SendMail&")) {
msg = true;
}
String us = makeUriString(cmd, extra);
HttpPost method = new HttpPost(URI.create(us));
if (cmd.startsWith("SendMail&")) {
if (msg) {
method.setHeader("Content-Type", "message/rfc822");
} else {
method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");

View File

@ -34,18 +34,25 @@ public class Rfc822OutputTests extends AndroidTestCase {
private static final String RECIPIENT_BCC = "recipient-bcc@android.com";
private static final String SUBJECT = "This is the subject";
private static final String BODY = "This is the body. This is also the body.";
private static final String TEXT = "Here is some new text.";
private static final String REPLY_BODY_SHORT = "\n\n" + SENDER + " wrote:\n\n";
private static final String REPLY_BODY = REPLY_BODY_SHORT + ">" + BODY;
// TODO Create more tests here. Specifically, we should test to make sure that forward works
// properly instead of just reply
// TODO Localize the following test, which will not work properly in other than English
// speaking locales!
/**
* Test for buildBodyText().
* Compare with expected values.
* Also test the situation where the message has no body.
*
* WARNING: This test is NOT localized, so it will fail if run on a device in a
* non-English speaking locale!
*/
public void testBuildBodyText() {
public void testBuildBodyTextWithReply() {
// Create the least necessary; sender, flags, and the body of the reply
Message msg = new Message();
msg.mText = "";
@ -54,14 +61,39 @@ public class Rfc822OutputTests extends AndroidTestCase {
msg.mTextReply = BODY;
msg.save(getContext());
String body = Rfc822Output.buildBodyText(getContext(), msg);
String body = Rfc822Output.buildBodyText(getContext(), msg, true);
assertEquals(REPLY_BODY, body);
// Save a different message with no reply body (so we reset the id)
msg.mId = -1;
msg.mTextReply = null;
msg.save(getContext());
body = Rfc822Output.buildBodyText(getContext(), msg);
body = Rfc822Output.buildBodyText(getContext(), msg, true);
assertEquals(REPLY_BODY_SHORT, body);
}
/**
* Test for buildBodyText().
* Compare with expected values.
* Also test the situation where the message has no body.
*/
public void testBuildBodyTextWithoutReply() {
// Create the least necessary; sender, flags, and the body of the reply
Message msg = new Message();
msg.mText = TEXT;
msg.mFrom = SENDER;
msg.mFlags = Message.FLAG_TYPE_REPLY;
msg.mTextReply = BODY;
msg.save(getContext());
String body = Rfc822Output.buildBodyText(getContext(), msg, false);
assertEquals(TEXT, body);
// Save a different message with no reply body (so we reset the id)
msg.mId = -1;
msg.mTextReply = null;
msg.save(getContext());
body = Rfc822Output.buildBodyText(getContext(), msg, false);
assertEquals(TEXT, body);
}
}