More work on account migration

* Split account copy loop to do POP3 accounts first, then IMAP
* After upgrading accounts, upgrade folders
* Upgrade messages in those folders
* Preserve attachments on outgoing messages (e.g. drafts)
* Enable composer and start syncing after upgrade
* Fix latent bug in LocalStore (which was not used in Eclair)
* Add tests for upgrade workers in LegacyConversions

Bug: 2065528
This commit is contained in:
Andrew Stadler 2010-03-08 13:52:09 -08:00
parent 5b69eb3e4b
commit fd249f61dd
7 changed files with 553 additions and 109 deletions

View File

@ -17,7 +17,9 @@
package com.android.email;
import com.android.email.mail.Address;
import com.android.email.mail.Body;
import com.android.email.mail.Flag;
import com.android.email.mail.Folder;
import com.android.email.mail.Message;
import com.android.email.mail.MessagingException;
import com.android.email.mail.Part;
@ -28,10 +30,12 @@ import com.android.email.mail.internet.MimeMessage;
import com.android.email.mail.internet.MimeMultipart;
import com.android.email.mail.internet.MimeUtility;
import com.android.email.mail.internet.TextBody;
import com.android.email.mail.store.LocalStore;
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.AttachmentColumns;
import com.android.email.provider.EmailContent.Mailbox;
import org.apache.commons.io.IOUtils;
@ -40,6 +44,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Log;
import java.io.File;
@ -48,12 +53,17 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
public class LegacyConversions {
/** DO NOT CHECK IN "TRUE" */
private static final boolean DEBUG_ATTACHMENTS = false;
/** Used for mapping folder names to type codes (e.g. inbox, drafts, trash) */
private static final HashMap<String, Integer>
sServerMailboxNames = new HashMap<String, Integer>();
/**
* Values for HEADER_ANDROID_BODY_QUOTED_PART to tag body parts
*/
@ -61,6 +71,15 @@ public class LegacyConversions {
/* package */ static final String BODY_QUOTED_PART_FORWARD = "quoted-forward";
/* package */ static final String BODY_QUOTED_PART_INTRO = "quoted-intro";
/**
* Standard columns for querying content providers
*/
private static final String[] ATTACHMENT_META_COLUMNS_PROJECTION = {
OpenableColumns.DISPLAY_NAME,
OpenableColumns.SIZE
};
private static final int ATTACHMENT_META_COLUMNS_SIZE = 1;
/**
* Copy field-by-field from a "store" message to a "provider" message
* @param message The message we've just downloaded (must be a MimeMessage)
@ -246,13 +265,14 @@ public class LegacyConversions {
* @param context a context for file operations
* @param localMessage the attachments will be built against this message
* @param attachments the attachments to add
* @param upgrading if true, we are upgrading a local account - handle attachments differently
* @throws IOException
*/
public static void updateAttachments(Context context, EmailContent.Message localMessage,
ArrayList<Part> attachments) throws MessagingException, IOException {
ArrayList<Part> attachments, boolean upgrading) throws MessagingException, IOException {
localMessage.mAttachments = null;
for (Part attachmentPart : attachments) {
addOneAttachment(context, localMessage, attachmentPart);
addOneAttachment(context, localMessage, attachmentPart, upgrading);
}
}
@ -272,10 +292,11 @@ public class LegacyConversions {
* @param context a context for file operations
* @param localMessage the attachments will be built against this message
* @param part a single attachment part from POP or IMAP
* @param upgrading true if upgrading a local account - handle attachments differently
* @throws IOException
*/
private static void addOneAttachment(Context context, EmailContent.Message localMessage,
Part part) throws MessagingException, IOException {
Part part, boolean upgrading) throws MessagingException, IOException {
Attachment localAttachment = new Attachment();
@ -287,13 +308,53 @@ public class LegacyConversions {
name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
}
// Try to pull size from disposition (if not downloaded)
// Select the URI for the new attachments. For attachments downloaded by the legacy
// IMAP/POP code, this is not determined yet, so is null (it will be rewritten below,
// or later, when the actual attachment file is created.)
//
// When upgrading older local accounts, the URI represents a local asset (e.g. a photo)
// so we need to preserve the URI.
// TODO This works for outgoing messages, where the URI does not change. May need
// additional logic to handle the case of rewriting URI for received attachments.
Uri contentUri = null;
String contentUriString = null;
if (upgrading) {
Body body = part.getBody();
if (body instanceof LocalStore.LocalAttachmentBody) {
LocalStore.LocalAttachmentBody localBody = (LocalStore.LocalAttachmentBody) body;
contentUri = localBody.getContentUri();
if (contentUri != null) {
contentUriString = contentUri.toString();
}
}
}
// Find size, if available, via a number of techniques:
long size = 0;
String disposition = part.getDisposition();
if (disposition != null) {
String s = MimeUtility.getHeaderParameter(disposition, "size");
if (s != null) {
size = Long.parseLong(s);
if (upgrading) {
// If upgrading a legacy account, the size must be recaptured from the data source
if (contentUri != null) {
Cursor metadataCursor = context.getContentResolver().query(contentUri,
ATTACHMENT_META_COLUMNS_PROJECTION, null, null, null);
if (metadataCursor != null) {
try {
if (metadataCursor.moveToFirst()) {
size = metadataCursor.getInt(ATTACHMENT_META_COLUMNS_SIZE);
}
} finally {
metadataCursor.close();
}
}
}
// TODO: a downloaded legacy attachment - see if the above code works
} else {
// Incoming attachment: Try to pull size from disposition (if not downloaded yet)
String disposition = part.getDisposition();
if (disposition != null) {
String s = MimeUtility.getHeaderParameter(disposition, "size");
if (s != null) {
size = Long.parseLong(s);
}
}
}
@ -306,7 +367,7 @@ public class LegacyConversions {
localAttachment.mMimeType = part.getMimeType();
localAttachment.mSize = size; // May be reset below if file handled
localAttachment.mContentId = part.getContentId();
localAttachment.mContentUri = null; // Will be set when file is saved
localAttachment.mContentUri = contentUriString;
localAttachment.mMessageKey = localMessage.mId;
localAttachment.mLocation = partId;
localAttachment.mEncoding = "B"; // TODO - convert other known encodings
@ -351,7 +412,9 @@ public class LegacyConversions {
}
// If an attachment body was actually provided, we need to write the file now
saveAttachmentBody(context, part, localAttachment, localMessage.mAccountKey);
if (!upgrading) {
saveAttachmentBody(context, part, localAttachment, localMessage.mAccountKey);
}
if (localMessage.mAttachments == null) {
localMessage.mAttachments = new ArrayList<Attachment>();
@ -610,4 +673,73 @@ public class LegacyConversions {
return result;
}
/**
* Conversion from legacy folder to provider mailbox. Used for account migration.
* Note: Many mailbox fields are unused in IMAP & POP accounts.
*
* @param context application context
* @param toAccount the provider account that this folder will be associated with
* @param fromFolder the legacy folder to convert to modern format
* @return an Account ready to be committed to provider
*/
public static EmailContent.Mailbox makeMailbox(Context context, EmailContent.Account toAccount,
Folder fromFolder) throws MessagingException {
EmailContent.Mailbox result = new EmailContent.Mailbox();
result.mDisplayName = fromFolder.getName();
// result.mServerId
// result.mParentServerId
result.mAccountKey = toAccount.mId;
result.mType = inferMailboxTypeFromName(context, fromFolder.getName());
// result.mDelimiter
// result.mSyncKey
// result.mSyncLookback
// result.mSyncInterval
result.mSyncTime = 0;
result.mUnreadCount = fromFolder.getUnreadMessageCount();
result.mFlagVisible = true;
result.mFlags = 0;
result.mVisibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
// result.mSyncStatus
return result;
}
/**
* Infer mailbox type from mailbox name. Used by MessagingController (for live folder sync)
* and for legacy account upgrades.
*/
public static synchronized int inferMailboxTypeFromName(Context context, String mailboxName) {
if (sServerMailboxNames.size() == 0) {
// preload the hashmap, one time only
sServerMailboxNames.put(
context.getString(R.string.mailbox_name_server_inbox).toLowerCase(),
Mailbox.TYPE_INBOX);
sServerMailboxNames.put(
context.getString(R.string.mailbox_name_server_outbox).toLowerCase(),
Mailbox.TYPE_OUTBOX);
sServerMailboxNames.put(
context.getString(R.string.mailbox_name_server_drafts).toLowerCase(),
Mailbox.TYPE_DRAFTS);
sServerMailboxNames.put(
context.getString(R.string.mailbox_name_server_trash).toLowerCase(),
Mailbox.TYPE_TRASH);
sServerMailboxNames.put(
context.getString(R.string.mailbox_name_server_sent).toLowerCase(),
Mailbox.TYPE_SENT);
sServerMailboxNames.put(
context.getString(R.string.mailbox_name_server_junk).toLowerCase(),
Mailbox.TYPE_JUNK);
}
if (mailboxName == null || mailboxName.length() == 0) {
return EmailContent.Mailbox.TYPE_MAIL;
}
String lowerCaseName = mailboxName.toLowerCase();
Integer type = sServerMailboxNames.get(lowerCaseName);
if (type != null) {
return type;
}
return EmailContent.Mailbox.TYPE_MAIL;
}
}

View File

@ -116,7 +116,6 @@ public class MessagingController implements Runnable {
private static MessagingController inst = null;
private BlockingQueue<Command> mCommands = new LinkedBlockingQueue<Command>();
private Thread mThread;
private final HashMap<String, Integer> mServerMailboxNames = new HashMap<String, Integer>();
/**
* All access to mListeners *must* be synchronized
@ -128,26 +127,6 @@ public class MessagingController implements Runnable {
protected MessagingController(Context _context) {
mContext = _context;
// Create lookup table for server-side mailbox names
mServerMailboxNames.put(
mContext.getString(R.string.mailbox_name_server_inbox).toLowerCase(),
Mailbox.TYPE_INBOX);
mServerMailboxNames.put(
mContext.getString(R.string.mailbox_name_server_outbox).toLowerCase(),
Mailbox.TYPE_OUTBOX);
mServerMailboxNames.put(
mContext.getString(R.string.mailbox_name_server_drafts).toLowerCase(),
Mailbox.TYPE_DRAFTS);
mServerMailboxNames.put(
mContext.getString(R.string.mailbox_name_server_trash).toLowerCase(),
Mailbox.TYPE_TRASH);
mServerMailboxNames.put(
mContext.getString(R.string.mailbox_name_server_sent).toLowerCase(),
Mailbox.TYPE_SENT);
mServerMailboxNames.put(
mContext.getString(R.string.mailbox_name_server_junk).toLowerCase(),
Mailbox.TYPE_JUNK);
mThread = new Thread(this);
mThread.start();
}
@ -341,7 +320,8 @@ public class MessagingController implements Runnable {
// box.mServerId;
// box.mParentServerId;
box.mAccountKey = account.mId;
box.mType = inferMailboxTypeFromName(account, remoteNameToAdd);
box.mType = LegacyConversions.inferMailboxTypeFromName(
mContext, remoteNameToAdd);
// box.mDelimiter;
// box.mSyncKey;
// box.mSyncLookback;
@ -366,23 +346,6 @@ public class MessagingController implements Runnable {
});
}
/**
* Temporarily: Infer mailbox type from mailbox name. This should probably be
* mutated into something that the stores can provide directly, instead of the two-step
* where we scan and report.
*/
public int inferMailboxTypeFromName(EmailContent.Account account, String mailboxName) {
if (mailboxName == null || mailboxName.length() == 0) {
return EmailContent.Mailbox.TYPE_MAIL;
}
String lowerCaseName = mailboxName.toLowerCase();
Integer type = mServerMailboxNames.get(lowerCaseName);
if (type != null) {
return type;
}
return EmailContent.Mailbox.TYPE_MAIL;
}
/**
* Start background synchronization of the specified folder.
* @param account
@ -1025,7 +988,7 @@ public class MessagingController implements Runnable {
// process (and save) attachments
LegacyConversions.updateAttachments(mContext, localMessage,
attachments);
attachments, false);
// One last update of message with two updated flags
localMessage.mFlagLoaded = loadStatus;

View File

@ -23,13 +23,18 @@ import com.android.email.Preferences;
import com.android.email.R;
import com.android.email.activity.setup.AccountSettingsUtils;
import com.android.email.activity.setup.AccountSettingsUtils.Provider;
import com.android.email.mail.FetchProfile;
import com.android.email.mail.Folder;
import com.android.email.mail.Message;
import com.android.email.mail.MessagingException;
import com.android.email.mail.Part;
import com.android.email.mail.Store;
import com.android.email.mail.internet.MimeUtility;
import com.android.email.mail.store.LocalStore;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.provider.EmailContent.HostAuth;
import com.android.email.provider.EmailContent.Mailbox;
import android.app.Activity;
import android.app.ListActivity;
@ -49,8 +54,11 @@ import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
/**
* This activity will be used whenever we have a large/slow bulk upgrade operation.
@ -62,10 +70,11 @@ import java.net.URISyntaxException;
* This allows it to continue through without restarting.
* Do not attempt to define orientation-specific resources, they won't be loaded.
*
* TODO: More work on actual conversions
* TODO: Confirm from donut sources the right way to ID the drafts, outbox, sent folders in IMAP
* TODO: Finish actual conversions
* TODO: POP3 inbox needs to handle local-delete sentinels
* TODO: Read pending events and convert them to things like updates or deletes in the DB
* TODO: Smarter cleanup of SSL/TLS situation, since certificates may be bad (see design spec)
* TODO: Trigger refresh after upgrade
* TODO: Close db (add to LocalStore) since we're getting so many DB warnings here
*/
public class UpgradeAccounts extends ListActivity implements OnClickListener {
@ -335,22 +344,46 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
}
}
// Step 3: Copy accounts (and delete old accounts)
// Step 3: Copy accounts (and delete old accounts). POP accounts first.
for (int i = 0; i < mAccountInfo.length; i++) {
if (mAccountInfo[i].error == null) {
copyAccount(mContext, mAccountInfo[i].account, i, handler);
}
deleteAccountStore(mContext, mAccountInfo[i].account, handler);
mAccountInfo[i].account.delete(mPreferences);
// reset the progress indicator to mark account "complete" (in case est was wrong)
UpgradeAccounts.this.mHandler.setMaxProgress(i, 100);
UpgradeAccounts.this.mHandler.setProgress(i, 100);
AccountInfo info = mAccountInfo[i];
copyAndDeleteAccount(info, i, handler, Store.STORE_SCHEME_POP3);
}
// IMAP accounts next.
for (int i = 0; i < mAccountInfo.length; i++) {
AccountInfo info = mAccountInfo[i];
copyAndDeleteAccount(info, i, handler, Store.STORE_SCHEME_IMAP);
}
// Step 4: Enable app-wide features such as composer, and start mail service(s)
Email.setServicesEnabled(mContext);
return null;
}
/**
* Copy and delete one account (helper for doInBackground). Can select accounts by type
* to force conversion of one or another type only.
*/
private void copyAndDeleteAccount(AccountInfo info, int i, UIHandler handler, String type) {
if (type != null) {
String storeUri = info.account.getStoreUri();
boolean isType = storeUri.startsWith(type);
if (!isType) {
return; // skip this account
}
}
if (info.error == null) {
copyAccount(mContext, info.account, i, handler);
}
deleteAccountStore(mContext, info.account, handler);
info.account.delete(mPreferences);
// reset the progress indicator to mark account "complete" (in case est was wrong)
handler.setMaxProgress(i, 100);
handler.setProgress(i, 100);
}
@Override
protected void onPostExecute(Void result) {
if (!isCancelled()) {
@ -377,6 +410,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
Folder folder = folders[i];
folder.open(Folder.OpenMode.READ_ONLY, null);
estimate += folder.getMessageCount();
folder.close(false);
}
estimate += ((LocalStore)store).getStoredAttachmentCount();
@ -419,6 +453,7 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
handler.incProgress(accountNum, 1 + messageCount);
}
}
folder.close(false);
}
int pruned = ((LocalStore)store).pruneCachedAttachments();
if (handler != null) {
@ -428,7 +463,17 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
Log.d(Email.LOG_TAG, "Exception while cleaning IMAP account " + e);
}
}
private static class FolderConversion {
final Folder folder;
final EmailContent.Mailbox mailbox;
public FolderConversion(Folder _folder, EmailContent.Mailbox _mailbox) {
folder = _folder;
mailbox = _mailbox;
}
}
/**
* Copy an account.
*/
@ -452,9 +497,147 @@ public class UpgradeAccounts extends ListActivity implements OnClickListener {
handler.incProgress(accountNum);
}
// TODO folders
// TODO messages
// TODO attachments
// copy the folders, making a set of them as we go, and recording a few that we
// need to process first (highest priority for saving the messages)
HashSet<FolderConversion> conversions = new HashSet<FolderConversion>();
FolderConversion drafts = null;
FolderConversion outbox = null;
FolderConversion sent = null;
try {
Store store = LocalStore.newInstance(account.getLocalStoreUri(), context, null);
Folder[] folders = store.getPersonalNamespaces();
for (Folder folder : folders) {
String folderName = null;
try {
folder.open(Folder.OpenMode.READ_ONLY, null);
folderName = folder.getName();
Log.d(Email.LOG_TAG, "Copy " + account.getDescription() + "." + folderName);
EmailContent.Mailbox mailbox =
LegacyConversions.makeMailbox(context, newAccount, folder);
mailbox.save(context);
if (handler != null) {
handler.incProgress(accountNum);
}
folder.close(false);
// Now record the conversion, to come back and do the messages
FolderConversion conversion = new FolderConversion(folder, mailbox);
conversions.add(conversion);
switch (mailbox.mType) {
case Mailbox.TYPE_DRAFTS:
drafts = conversion;
break;
case Mailbox.TYPE_OUTBOX:
outbox = conversion;
break;
case Mailbox.TYPE_SENT:
sent = conversion;
break;
}
} catch (MessagingException e) {
// We make a best-effort attempt at each folder, so even if this one fails,
// we'll try to keep going.
Log.d(Email.LOG_TAG, "Exception copying folder " + folderName + ": " + e);
if (handler != null) {
handler.error(context.getString(R.string.upgrade_accounts_error));
}
}
}
} catch (MessagingException e) {
Log.d(Email.LOG_TAG, "Exception while copying folders " + e);
// Couldn't copy folders at all
if (handler != null) {
handler.error(context.getString(R.string.upgrade_accounts_error));
}
}
// copy the messages, starting with the most critical folders, and then doing the rest
// outbox & drafts are the most important, as they don't exist anywhere else
if (outbox != null) {
copyMessages(context, outbox, true, newAccount.mId, accountNum, handler);
conversions.remove(outbox);
}
if (drafts != null) {
copyMessages(context, drafts, true, newAccount.mId, accountNum, handler);
conversions.remove(drafts);
}
if (sent != null) {
copyMessages(context, sent, true, newAccount.mId, accountNum, handler);
conversions.remove(outbox);
}
// Now handle any remaining folders
for (FolderConversion conversion : conversions) {
copyMessages(context, conversion, false, newAccount.mId, accountNum, handler);
}
}
/**
* Copy all messages in a given folder
*
* @param context a system context
* @param conversion a folder->mailbox conversion record
* @param localAttachments true if the attachments refer to local data (to be sent)
* @param newAccountId the id of the newly-created account
* @param accountNum the UI list # of the account
* @param handler the handler for updating the UI
*/
/* package */ static void copyMessages(Context context, FolderConversion conversion,
boolean localAttachments, long newAccountId, int accountNum, UIHandler handler) {
try {
conversion.folder.open(Folder.OpenMode.READ_ONLY, null);
Message[] oldMessages = conversion.folder.getMessages(null);
for (Message oldMessage : oldMessages) {
Exception e = null;
try {
// load message data from legacy Store
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.BODY);
conversion.folder.fetch(new Message[] { oldMessage }, fp, null);
// convert message (headers)
EmailContent.Message newMessage = new EmailContent.Message();
LegacyConversions.updateMessageFields(newMessage, oldMessage, newAccountId,
conversion.mailbox.mId);
// convert body (text)
EmailContent.Body newBody = new EmailContent.Body();
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(oldMessage, viewables, attachments);
LegacyConversions.updateBodyFields(newBody, newMessage, viewables);
// commit changes so far so we have real id's
newMessage.save(context);
newBody.save(context);
// convert attachments
if (localAttachments) {
// These are references to local data, and should create records only
// (e.g. the content URI). No files should be created.
LegacyConversions.updateAttachments(context, newMessage, attachments, true);
} else {
// TODO handle downloaded attachments
}
// done
if (handler != null) {
handler.incProgress(accountNum);
}
} catch (MessagingException me) {
e = me;
} catch (IOException ioe) {
e = ioe;
}
if (e != null) {
Log.d(Email.LOG_TAG, "Exception copying message " + oldMessage.getSubject()
+ ": "+ e);
if (handler != null) {
handler.error(context.getString(R.string.upgrade_accounts_error));
}
}
}
} catch (MessagingException e) {
// Couldn't copy messages at all
Log.d(Email.LOG_TAG, "Exception while copying messages " + e);
if (handler != null) {
handler.error(context.getString(R.string.upgrade_accounts_error));
}
}
}
/**

View File

@ -25,6 +25,8 @@ import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.regex.Pattern;
@ -439,7 +441,7 @@ public class Address {
* as found in LocalStore (Donut; db version up to 24).
* @See unpack()
*/
/* package */ static Address[] legacyUnpack(String addressList) {
public static Address[] legacyUnpack(String addressList) {
if (addressList == null || addressList.length() == 0) {
return new Address[] { };
}
@ -471,4 +473,35 @@ public class Address {
}
return addresses.toArray(new Address[] { });
}
/**
* Legacy pack() used for writing to old data (migration),
* as found in LocalStore (Donut; db version up to 24).
* @See unpack()
*/
public static String legacyPack(Address[] addresses) {
if (addresses == null) {
return null;
} else if (addresses.length == 0) {
return "";
}
StringBuffer sb = new StringBuffer();
for (int i = 0, count = addresses.length; i < count; i++) {
Address address = addresses[i];
try {
sb.append(URLEncoder.encode(address.getAddress(), "UTF-8"));
if (address.getPersonal() != null) {
sb.append(';');
sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8"));
}
if (i < count - 1) {
sb.append(',');
}
}
catch (UnsupportedEncodingException uee) {
return null;
}
}
return sb.toString();
}
}

View File

@ -921,7 +921,7 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
private void populateMessageFromGetMessageCursor(LocalMessage message, Cursor cursor)
throws MessagingException{
message.setSubject(cursor.getString(0) == null ? "" : cursor.getString(0));
Address[] from = Address.unpack(cursor.getString(1));
Address[] from = Address.legacyUnpack(cursor.getString(1));
if (from.length > 0) {
message.setFrom(from[0]);
}
@ -938,10 +938,10 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
}
}
message.mId = cursor.getLong(5);
message.setRecipients(RecipientType.TO, Address.unpack(cursor.getString(6)));
message.setRecipients(RecipientType.CC, Address.unpack(cursor.getString(7)));
message.setRecipients(RecipientType.BCC, Address.unpack(cursor.getString(8)));
message.setReplyTo(Address.unpack(cursor.getString(9)));
message.setRecipients(RecipientType.TO, Address.legacyUnpack(cursor.getString(6)));
message.setRecipients(RecipientType.CC, Address.legacyUnpack(cursor.getString(7)));
message.setRecipients(RecipientType.BCC, Address.legacyUnpack(cursor.getString(8)));
message.setReplyTo(Address.legacyUnpack(cursor.getString(9)));
message.mAttachmentCount = cursor.getInt(10);
message.setInternalDate(new Date(cursor.getLong(11)));
message.setMessageId(cursor.getString(12));
@ -1190,17 +1190,18 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
ContentValues cv = new ContentValues();
cv.put("uid", message.getUid());
cv.put("subject", message.getSubject());
cv.put("sender_list", Address.pack(message.getFrom()));
cv.put("sender_list", Address.legacyPack(message.getFrom()));
cv.put("date", message.getSentDate() == null
? System.currentTimeMillis() : message.getSentDate().getTime());
cv.put("flags", makeFlagsString(message));
cv.put("folder_id", mFolderId);
cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO)));
cv.put("cc_list", Address.pack(message.getRecipients(RecipientType.CC)));
cv.put("bcc_list", Address.pack(message.getRecipients(RecipientType.BCC)));
cv.put("to_list", Address.legacyPack(message.getRecipients(RecipientType.TO)));
cv.put("cc_list", Address.legacyPack(message.getRecipients(RecipientType.CC)));
cv.put("bcc_list", Address.legacyPack(
message.getRecipients(RecipientType.BCC)));
cv.put("html_content", sbHtml.length() > 0 ? sbHtml.toString() : null);
cv.put("text_content", sbText.length() > 0 ? sbText.toString() : null);
cv.put("reply_to_list", Address.pack(message.getReplyTo()));
cv.put("reply_to_list", Address.legacyPack(message.getReplyTo()));
cv.put("attachment_count", attachments.size());
cv.put("internal_date", message.getInternalDate() == null
? System.currentTimeMillis() : message.getInternalDate().getTime());
@ -1272,21 +1273,21 @@ public class LocalStore extends Store implements PersistentDataCallbacks {
new Object[] {
message.getUid(),
message.getSubject(),
Address.pack(message.getFrom()),
Address.legacyPack(message.getFrom()),
message.getSentDate() == null ? System
.currentTimeMillis() : message.getSentDate()
.getTime(),
makeFlagsString(message),
mFolderId,
Address.pack(message
Address.legacyPack(message
.getRecipients(RecipientType.TO)),
Address.pack(message
Address.legacyPack(message
.getRecipients(RecipientType.CC)),
Address.pack(message
Address.legacyPack(message
.getRecipients(RecipientType.BCC)),
sbHtml.length() > 0 ? sbHtml.toString() : null,
sbText.length() > 0 ? sbText.toString() : null,
Address.pack(message.getReplyTo()),
Address.legacyPack(message.getReplyTo()),
attachments.size(),
message.getMessageId(),
makeFlagNumeric(message, Flag.X_STORE_1),

View File

@ -16,14 +16,16 @@
package com.android.email;
import com.android.email.Account;
import com.android.email.mail.Address;
import com.android.email.mail.Body;
import com.android.email.mail.BodyPart;
import com.android.email.mail.Flag;
import com.android.email.mail.Folder;
import com.android.email.mail.Message;
import com.android.email.mail.MessageTestUtils;
import com.android.email.mail.MessagingException;
import com.android.email.mail.Part;
import com.android.email.mail.Folder.OpenMode;
import com.android.email.mail.Message.RecipientType;
import com.android.email.mail.MessageTestUtils.MessageBuilder;
import com.android.email.mail.MessageTestUtils.MultipartBuilder;
@ -32,10 +34,13 @@ import com.android.email.mail.internet.MimeHeader;
import com.android.email.mail.internet.MimeMessage;
import com.android.email.mail.internet.MimeUtility;
import com.android.email.mail.internet.TextBody;
import com.android.email.mail.store.LocalStore;
import com.android.email.mail.store.LocalStoreUnitTests;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailProvider;
import com.android.email.provider.ProviderTestUtils;
import com.android.email.provider.EmailContent.Attachment;
import com.android.email.provider.EmailContent.Mailbox;
import android.content.ContentUris;
import android.content.Context;
@ -208,13 +213,13 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
"local-message", accountId, mailboxId, false, true, mProviderContext);
// Prepare a legacy message with attachments
final Message legacyMessage = prepareLegacyMessageWithAttachments(2);
final Message legacyMessage = prepareLegacyMessageWithAttachments(2, false);
// Now, convert from legacy to provider and see what happens
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(legacyMessage, viewables, attachments);
LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments);
LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments, false);
// Read back all attachments for message and check field values
Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
@ -249,50 +254,115 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
"local-message", accountId, mailboxId, false, true, mProviderContext);
// Prepare a legacy message with attachments
Message legacyMessage = prepareLegacyMessageWithAttachments(2);
Message legacyMessage = prepareLegacyMessageWithAttachments(2, false);
// Now, convert from legacy to provider and see what happens
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(legacyMessage, viewables, attachments);
LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments);
LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments, false);
// Confirm two attachment objects created
Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
assertEquals(2, EmailContent.count(mProviderContext, uri, null, null));
// Now add the attachments again and confirm there are still only two
LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments);
LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments, false);
assertEquals(2, EmailContent.count(mProviderContext, uri, null, null));
// Now add a 3rd & 4th attachment and make sure the total is 4, not 2 or 6
legacyMessage = prepareLegacyMessageWithAttachments(4);
legacyMessage = prepareLegacyMessageWithAttachments(4, false);
viewables = new ArrayList<Part>();
attachments = new ArrayList<Part>();
MimeUtility.collectParts(legacyMessage, viewables, attachments);
LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments);
LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments, false);
assertEquals(4, EmailContent.count(mProviderContext, uri, null, null));
}
/**
* Prepare a legacy message with 1+ attachments
* Sunny day test of adding attachments in "local account upgrade" mode
*/
private static Message prepareLegacyMessageWithAttachments(int numAttachments)
public void testLocalUpgradeAttachments() throws MessagingException, IOException {
// Prepare a local message to add the attachments to
final long accountId = 1;
final long mailboxId = 1;
final EmailContent.Message localMessage = ProviderTestUtils.setupMessage(
"local-upgrade", accountId, mailboxId, false, true, mProviderContext);
// Prepare a legacy message with attachments
final Message legacyMessage = prepareLegacyMessageWithAttachments(2, true);
// Now, convert from legacy to provider and see what happens
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
MimeUtility.collectParts(legacyMessage, viewables, attachments);
LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments, true);
// Read back all attachments for message and check field values
Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
Cursor c = mProviderContext.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION,
null, null, null);
try {
assertEquals(2, c.getCount());
while (c.moveToNext()) {
Attachment attachment = Attachment.getContent(c, Attachment.class);
// This attachment should look as if created by modern (provider) MessageCompose.
// 1. find the original that it was created from
Part fromPart = null;
for (Part from : attachments) {
String contentType = MimeUtility.unfoldAndDecode(from.getContentType());
String name = MimeUtility.getHeaderParameter(contentType, "name");
if (name.equals(attachment.mFileName)) {
fromPart = from;
break;
}
}
assertTrue(fromPart != null);
// 2. Check values
checkAttachment(attachment.mFileName, fromPart, attachment);
}
} finally {
c.close();
}
}
/**
* Prepare a legacy message with 1+ attachments
* @param numAttachments how many attachments to add
* @param localData if true, attachments are "local" data. false = "remote" (from server)
*/
private Message prepareLegacyMessageWithAttachments(int numAttachments, boolean localData)
throws MessagingException {
// First, build one or more attachment parts
MultipartBuilder mpBuilder = new MultipartBuilder("multipart/mixed");
for (int i = 1; i <= numAttachments; ++i) {
BodyPart attachmentPart = MessageTestUtils.bodyPart("image/jpg", null);
if (localData) {
// generate an attachment that was generated by legacy code (e.g. donut)
// for test of upgrading accounts in place
// This creator models the code in legacy MessageCompose
Uri uri = Uri.parse("content://test/attachment/" + i);
String quotedName = "\"test-attachment-" + i + "\"";
MimeBodyPart bp = new MimeBodyPart(
new LocalStore.LocalAttachmentBody(uri, mProviderContext));
bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "image/jpg;\n name=" + quotedName);
bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
"attachment;\n filename=" + quotedName);
mpBuilder.addBodyPart(bp);
} else {
// generate an attachment that came from a server
BodyPart attachmentPart = MessageTestUtils.bodyPart("image/jpg", null);
// name=attachmentN size=N00 location=10N
attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
"image/jpg;\n name=\"attachment" + i + "\"");
attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
"attachment;\n filename=\"attachment2\";\n size=" + i + "00");
attachmentPart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "10" + i);
// name=attachmentN size=N00 location=10N
attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
"image/jpg;\n name=\"attachment" + i + "\"");
attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
"attachment;\n filename=\"attachment2\";\n size=" + i + "00");
attachmentPart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "10" + i);
mpBuilder.addBodyPart(attachmentPart);
mpBuilder.addBodyPart(attachmentPart);
}
}
// Now build a message with them
@ -338,13 +408,30 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
assertEquals(tag, expected.getMimeType(), actual.mMimeType);
String disposition = expected.getDisposition();
String sizeString = MimeUtility.getHeaderParameter(disposition, "size");
long expectedSize = Long.parseLong(sizeString);
long expectedSize = (sizeString != null) ? Long.parseLong(sizeString) : 0;
assertEquals(tag, expectedSize, actual.mSize);
assertEquals(tag, expected.getContentId(), actual.mContentId);
assertNull(tag, actual.mContentUri);
// content URI either both null or both matching
String expectedUriString = null;
Body body = expected.getBody();
if (body instanceof LocalStore.LocalAttachmentBody) {
LocalStore.LocalAttachmentBody localBody = (LocalStore.LocalAttachmentBody) body;
Uri contentUri = localBody.getContentUri();
if (contentUri != null) {
expectedUriString = contentUri.toString();
}
}
assertEquals(tag, expectedUriString, actual.mContentUri);
assertTrue(tag, 0 != actual.mMessageKey);
String expectedPartId =
expected.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA)[0];
// location is either both null or both matching
String expectedPartId = null;
String[] storeData = expected.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
if (storeData != null && storeData.length > 0) {
expectedPartId = storeData[0];
}
assertEquals(tag, expectedPartId, actual.mLocation);
assertEquals(tag, "B", actual.mEncoding);
}
@ -618,4 +705,49 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
assertEquals(tag + " security", expect.mSecurityFlags, actual.mSecurityFlags);
assertEquals(tag + " signature", expect.mSignature, actual.mSignature);
}
/**
* Test conversion of a legacy mailbox to a provider mailbox
*/
public void testMakeProviderMailbox() throws MessagingException {
EmailContent.Account toAccount = ProviderTestUtils.setupAccount("convert-mailbox",
true, mProviderContext);
Folder fromFolder = buildTestFolder("INBOX");
Mailbox toMailbox = LegacyConversions.makeMailbox(mProviderContext, toAccount, fromFolder);
// Now test fields in created mailbox
assertEquals("INBOX", toMailbox.mDisplayName);
assertNull(toMailbox.mServerId);
assertNull(toMailbox.mParentServerId);
assertEquals(toAccount.mId, toMailbox.mAccountKey);
assertEquals(Mailbox.TYPE_INBOX, toMailbox.mType);
assertEquals(0, toMailbox.mDelimiter);
assertNull(toMailbox.mSyncKey);
assertEquals(0, toMailbox.mSyncLookback);
assertEquals(0, toMailbox.mSyncInterval);
assertEquals(0, toMailbox.mSyncTime);
assertEquals(100, toMailbox.mUnreadCount);
assertTrue(toMailbox.mFlagVisible);
assertEquals(0, toMailbox.mFlags);
assertEquals(Email.VISIBLE_LIMIT_DEFAULT, toMailbox.mVisibleLimit);
assertNull(toMailbox.mSyncStatus);
}
/**
* Build a lightweight Store Folder with simple field population. The folder is "open"
* and should be closed by the caller.
*/
private Folder buildTestFolder(String folderName) throws MessagingException {
String localStoreUri =
"local://localhost/" + mProviderContext.getDatabasePath(LocalStoreUnitTests.DB_NAME);
LocalStore store = (LocalStore) LocalStore.newInstance(localStoreUri, getContext(), null);
LocalStore.LocalFolder folder = (LocalStore.LocalFolder) store.getFolder(folderName);
folder.open(OpenMode.READ_WRITE, null); // this will create it
// set a few fields to test values
// folder.getName - set by getFolder()
folder.setUnreadMessageCount(100);
return folder;
}
}

View File

@ -56,7 +56,7 @@ import java.util.HashSet;
@MediumTest
public class LocalStoreUnitTests extends AndroidTestCase {
private static final String dbName = "com.android.email.mail.store.LocalStoreUnitTests.db";
public static final String DB_NAME = "com.android.email.mail.store.LocalStoreUnitTests.db";
private static final String SENDER = "sender@android.com";
private static final String RECIPIENT_TO = "recipient-to@android.com";
@ -85,7 +85,7 @@ public class LocalStoreUnitTests extends AndroidTestCase {
// These are needed so we can get at the inner classes
// Create a dummy database (be sure to delete it in tearDown())
mLocalStoreUri = "local://localhost/" + getContext().getDatabasePath(dbName);
mLocalStoreUri = "local://localhost/" + getContext().getDatabasePath(DB_NAME);
mStore = (LocalStore) LocalStore.newInstance(mLocalStoreUri, getContext(), null);
mFolder = (LocalStore.LocalFolder) mStore.getFolder(FOLDER_NAME);