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:
parent
42c130dcf1
commit
1d98989222
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user