diff --git a/Android.mk b/Android.mk index 7f0a29940..c59ed30ae 100644 --- a/Android.mk +++ b/Android.mk @@ -22,11 +22,18 @@ include $(CLEAR_VARS) chips_dir := ../../../frameworks/ex/chips/res mail_common_dir := ../../../frameworks/opt/mailcommon/res res_dir := $(chips_dir) $(mail_common_dir) res +unified_email_src_dir := ../UnifiedEmail/src + +imported_unified_email_files := \ + $(unified_email_src_dir)/com/android/mail/providers/UIProviderValidator.java \ + $(unified_email_src_dir)/com/android/mail/providers/UIProvider.java LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/email) LOCAL_SRC_FILES += $(call all-java-files-under, src/com/beetstra) +LOCAL_SRC_FILES += $(imported_unified_email_files) + LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dir)) LOCAL_AAPT_FLAGS := --auto-add-overlay LOCAL_AAPT_FLAGS += --extra-packages com.android.ex.chips diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 39754a249..bf52778e5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -75,9 +75,10 @@ android:name="android.permission.USE_CREDENTIALS"/> + - + diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index 1eab50bb5..9525e2023 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -23,8 +23,8 @@ import android.content.ContentProviderResult; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; -import android.content.Intent; import android.content.Context; +import android.content.Intent; import android.content.OperationApplicationException; import android.content.UriMatcher; import android.database.ContentObserver; @@ -35,10 +35,13 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; +import android.os.Debug; +import android.provider.BaseColumns; import android.provider.ContactsContract; import android.text.TextUtils; import android.util.Log; +import com.android.common.content.ProjectionMap; import com.android.email.Email; import com.android.email.Preferences; import com.android.email.provider.ContentCache.CacheToken; @@ -65,6 +68,7 @@ import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.provider.Policy; import com.android.emailcommon.provider.QuickResponse; import com.android.emailcommon.service.LegacyPolicySet; +import com.android.mail.providers.UIProvider; import com.google.common.annotations.VisibleForTesting; import java.io.File; @@ -93,7 +97,7 @@ public class EmailProvider extends ContentProvider { * is NOT the preferred way of getting notification. */ public static final String ACTION_NOTIFY_MESSAGE_LIST_DATASET_CHANGED = - "com.android.email.MESSAGE_LIST_DATASET_CHANGED"; + "com.android.email.MESSAGE_LIST_DATASET_CHANGED"; public static final String EMAIL_MESSAGE_MIME_TYPE = "vnd.android.cursor.item/email-message"; @@ -226,8 +230,13 @@ public class EmailProvider extends ContentProvider { private static final int QUICK_RESPONSE_ID = QUICK_RESPONSE_BASE + 1; private static final int QUICK_RESPONSE_ACCOUNT_ID = QUICK_RESPONSE_BASE + 2; + private static final int UI_BASE = 0x9000; + private static final int UI_FOLDERS = UI_BASE; + private static final int UI_MESSAGES = UI_BASE + 1; + private static final int UI_MESSAGE = UI_BASE + 2; + // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS - private static final int LAST_EMAIL_PROVIDER_DB_BASE = QUICK_RESPONSE_BASE; + private static final int LAST_EMAIL_PROVIDER_DB_BASE = UI_BASE; // DO NOT CHANGE BODY_BASE!! private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000; @@ -248,7 +257,8 @@ public class EmailProvider extends ContentProvider { Message.DELETED_TABLE_NAME, Policy.TABLE_NAME, QuickResponse.TABLE_NAME, - Body.TABLE_NAME + Body.TABLE_NAME, + null }; // CONTENT_CACHES MUST remain in the order of the BASE constants above @@ -262,7 +272,8 @@ public class EmailProvider extends ContentProvider { null, // Deleted message mCachePolicy, null, // Quick response - null // Body + null, // Body + null // UI }; // CACHE_PROJECTIONS MUST remain in the order of the BASE constants above @@ -276,7 +287,8 @@ public class EmailProvider extends ContentProvider { null, // Deleted message Policy.CONTENT_PROJECTION, null, // Quick response - null // Body + null, // Body + null // UI }; private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); @@ -428,6 +440,10 @@ public class EmailProvider extends ContentProvider { // All quick responses associated with a particular account id matcher.addURI(EmailContent.AUTHORITY, "quickresponse/account/#", QUICK_RESPONSE_ACCOUNT_ID); + + matcher.addURI(EmailContent.AUTHORITY, "uifolders/*", UI_FOLDERS); + matcher.addURI(EmailContent.AUTHORITY, "uimessages/#", UI_MESSAGES); + matcher.addURI(EmailContent.AUTHORITY, "uimessage/#", UI_MESSAGE); } /** @@ -1744,7 +1760,6 @@ public class EmailProvider extends ContentProvider { bodyFile.delete(); } } - @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { @@ -1798,6 +1813,15 @@ public class EmailProvider extends ContentProvider { try { switch (match) { + // First, dispatch queries from UnfiedEmail + case UI_FOLDERS: + case UI_MESSAGES: + case UI_MESSAGE: + // For now, we don't allow selection criteria within these queries + if (selection != null || selectionArgs != null) { + throw new IllegalArgumentException("UI queries can't have selection/args"); + } + return uiQuery(match, uri, projection); case ACCOUNT_DEFAULT_ID: // Start with a snapshot of the cache Map accountCache = mCacheAccount.getSnapshot(); @@ -2641,7 +2665,7 @@ outer: } } - /** + /** * For testing purposes, check whether a given row is cached * @param baseUri the base uri of the EmailContent * @param id the row id of the EmailContent @@ -2680,4 +2704,223 @@ outer: public void injectAttachmentService(AttachmentService as) { mAttachmentService = (as == null) ? DEFAULT_ATTACHMENT_SERVICE : as; } + + /** + * Support for UnifiedEmail below + */ + + /** + * Mapping of UIProvider columns to EmailProvider columns for the message list (called the + * conversation list in UnifiedEmail) + */ + private static final ProjectionMap sMessageListMap = ProjectionMap.builder() + .add(BaseColumns._ID, MessageColumns.ID) + .add(UIProvider.ConversationColumns.URI, uriWithId("uimessage")) + .add(UIProvider.ConversationColumns.MESSAGE_LIST_URI, uriWithId("uimessage")) + .add(UIProvider.ConversationColumns.SUBJECT, MessageColumns.SUBJECT) + .add(UIProvider.ConversationColumns.SNIPPET, MessageColumns.SNIPPET) + .add(UIProvider.ConversationColumns.SENDER_INFO, MessageColumns.FROM_LIST) + .add(UIProvider.ConversationColumns.DATE_RECEIVED_MS, MessageColumns.TIMESTAMP) + .add(UIProvider.ConversationColumns.HAS_ATTACHMENTS, MessageColumns.FLAG_ATTACHMENT) + .build(); + + /** + * Mapping of UIProvider columns to EmailProvider columns for a detailed message view in + * UnifiedEmail + */ + private static final ProjectionMap sMessageViewMap = ProjectionMap.builder() + .add(BaseColumns._ID, Message.TABLE_NAME + "." + EmailContent.MessageColumns.ID) + .add(UIProvider.MessageColumns.SERVER_ID, SyncColumns.SERVER_ID) + .add(UIProvider.MessageColumns.URI, uriWithFQId("uimessage", Message.TABLE_NAME)) + .add(UIProvider.MessageColumns.CONVERSATION_ID, + uriWithFQId("uimessage", Message.TABLE_NAME)) + .add(UIProvider.MessageColumns.SUBJECT, EmailContent.MessageColumns.SUBJECT) + .add(UIProvider.MessageColumns.SNIPPET, EmailContent.MessageColumns.SNIPPET) + .add(UIProvider.MessageColumns.FROM, EmailContent.MessageColumns.FROM_LIST) + .add(UIProvider.MessageColumns.TO, EmailContent.MessageColumns.TO_LIST) + .add(UIProvider.MessageColumns.CC, EmailContent.MessageColumns.CC_LIST) + .add(UIProvider.MessageColumns.BCC, EmailContent.MessageColumns.BCC_LIST) + .add(UIProvider.MessageColumns.REPLY_TO, EmailContent.MessageColumns.REPLY_TO_LIST) + .add(UIProvider.MessageColumns.DATE_RECEIVED_MS, EmailContent.MessageColumns.TIMESTAMP) + .add(UIProvider.MessageColumns.BODY_HTML, Body.HTML_CONTENT) + .add(UIProvider.MessageColumns.BODY_TEXT, Body.TEXT_CONTENT) + .add(UIProvider.MessageColumns.EMBEDS_EXTERNAL_RESOURCES, "0") + .add(UIProvider.MessageColumns.REF_MESSAGE_ID, "0") + .add(UIProvider.MessageColumns.DRAFT_TYPE, "0") + .add(UIProvider.MessageColumns.INCLUDE_QUOTED_TEXT, "0") + .add(UIProvider.MessageColumns.QUOTE_START_POS, "0") + .add(UIProvider.MessageColumns.CLIENT_CREATED, "0") + .add(UIProvider.MessageColumns.CUSTOM_FROM_ADDRESS, "0") + .add(UIProvider.MessageColumns.HAS_ATTACHMENTS, EmailContent.MessageColumns.FLAG_ATTACHMENT) + .add(UIProvider.MessageColumns.INCLUDE_QUOTED_TEXT, "0") + .add(UIProvider.MessageColumns.ATTACHMENT_LIST_URI, + uriWithFQId("uiattachments", Message.TABLE_NAME)) + .add(UIProvider.MessageColumns.MESSAGE_FLAGS, "0") + .build(); + + /** + * Mapping of UIProvider columns to EmailProvider columns for the folder list in UnifiedEmail + */ + private static final ProjectionMap sFolderListMap = ProjectionMap.builder() + .add(BaseColumns._ID, MessageColumns.ID) + .add(UIProvider.FolderColumns.URI, uriWithId("uifolder")) + .add(UIProvider.FolderColumns.NAME, "displayName") + .add(UIProvider.FolderColumns.HAS_CHILDREN, "0") + .add(UIProvider.FolderColumns.CAPABILITIES, "0") + .add(UIProvider.FolderColumns.SYNC_FREQUENCY, "0") + .add(UIProvider.FolderColumns.SYNC_WINDOW, "3") + .add(UIProvider.FolderColumns.CONVERSATION_LIST_URI, uriWithId("uimessages")) + .add(UIProvider.FolderColumns.CHILD_FOLDERS_LIST_URI, uriWithId("uichildren")) + .add(UIProvider.FolderColumns.UNREAD_COUNT, "7") + .add(UIProvider.FolderColumns.TOTAL_COUNT, "77") + .build(); + + /** + * The "ORDER BY" clause for top level folders + */ + private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE + + " WHEN " + Mailbox.TYPE_INBOX + " THEN 0" + + " WHEN " + Mailbox.TYPE_DRAFTS + " THEN 1" + + " WHEN " + Mailbox.TYPE_OUTBOX + " THEN 2" + + " WHEN " + Mailbox.TYPE_SENT + " THEN 3" + + " WHEN " + Mailbox.TYPE_TRASH + " THEN 4" + + " WHEN " + Mailbox.TYPE_JUNK + " THEN 5" + // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order. + + " ELSE 10 END" + + " ," + MailboxColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; + + /** + * Generate the SELECT clause using a specified mapping and the original UI projection + * @param map the ProjectionMap to use for this projection + * @param projection the projection as sent by UnifiedEmail + * @return a StringBuilder containing the SELECT expression for a SQLite query + */ + private StringBuilder genSelect(ProjectionMap map, String[] projection) { + StringBuilder sb = new StringBuilder("SELECT "); + boolean first = true; + for (String column: projection) { + if (first) { + first = false; + } else { + sb.append(','); + } + sb.append(map.get(column)); + } + return sb; + } + + /** + * Convenience method to create a Uri string given the "type" of query; we append the type + * of the query and the id column name (_id) + * + * @param type the "type" of the query, as defined by our UriMatcher definitions + * @return a Uri string + */ + private static String uriWithId(String type) { + return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || _id"; + } + + /** + * Convenience method to create a Uri string given the "type" of query and the table name to + * which it applies; we append the type of the query and the fully qualified (FQ) id column + * (i.e. including the table name); we need this for join queries where _id would otherwise + * be ambiguous + * + * @param type the "type" of the query, as defined by our UriMatcher definitions + * @param tableName the name of the table whose _id is referred to + * @return a Uri string + */ + private static String uriWithFQId(String type, String tableName) { + return "'content://" + EmailContent.AUTHORITY + "/" + type + "/' || " + tableName + "._id"; + } + + /** + * Generate the "view message" SQLite query, given a projection from UnifiedEmail + * + * @param uiProjection as passed from UnifiedEmail + * @return the SQLite query to be executed on the EmailProvider database + */ + private String genQueryViewMessage(String[] uiProjection) { + StringBuilder sb = genSelect(sMessageViewMap, uiProjection); + sb.append(" FROM " + Message.TABLE_NAME + "," + Body.TABLE_NAME + " WHERE " + + Body.MESSAGE_KEY + "=" + Message.TABLE_NAME + "." + Message.RECORD_ID + " AND " + + Message.TABLE_NAME + "." + Message.RECORD_ID + "=?"); + return sb.toString(); + } + + /** + * Generate the "message list" SQLite query, given a projection from UnifiedEmail + * + * @param uiProjection as passed from UnifiedEmail + * @return the SQLite query to be executed on the EmailProvider database + */ + private String genQueryMailboxMessages(String[] uiProjection) { + StringBuilder sb = genSelect(sMessageListMap, uiProjection); + // Make constant + sb.append(" FROM " + Message.TABLE_NAME + " WHERE " + Message.MAILBOX_KEY + "=?"); + return sb.toString(); + } + + /** + * Generate the "folder list" SQLite query, given a projection from UnifiedEmail + * + * @param uiProjection as passed from UnifiedEmail + * @return the SQLite query to be executed on the EmailProvider database + */ + private String genQueryAccountMailboxes(String[] uiProjection) { + StringBuilder sb = genSelect(sFolderListMap, uiProjection); + // Make constant + sb.append(" FROM " + Mailbox.TABLE_NAME + " WHERE " + Mailbox.ACCOUNT_KEY + "=? ORDER BY "); + sb.append(MAILBOX_ORDER_BY); + return sb.toString(); + } + + /** + * Given the email address of an account, return its account id (the _id row in the Account + * table), or NO_ACCOUNT (-1) if not found + * + * @param email the email address of the account + * @return the account id for this account, or NO_ACCOUNT if not found + */ + private long findAccountIdByName(String email) { + Map accountCache = mCacheAccount.getSnapshot(); + Collection accounts = accountCache.values(); + for (Cursor accountCursor: accounts) { + if (accountCursor.getString(Account.CONTENT_EMAIL_ADDRESS_COLUMN).equals(email)) { + return accountCursor.getLong(Account.CONTENT_ID_COLUMN); + } + } + return Account.NO_ACCOUNT; + } + + /** + * Handle UnifiedEmail queries here (dispatched from query()) + * + * @param match the UriMatcher match for the original uri passed in from UnifiedEmail + * @param uri the original uri passed in from UnifiedEmail + * @param uiProjection the projection passed in from UnifiedEmail + * @return the result Cursor + */ + private Cursor uiQuery(int match, Uri uri, String[] uiProjection) { + Context context = getContext(); + SQLiteDatabase db = getDatabase(context); + switch(match) { + case UI_FOLDERS: + // We are passed the email address (unique account identifier) in the uri; we + // need to turn this into the _id of the Account row in the EmailProvider db + String accountName = uri.getPathSegments().get(1); + long acctId = findAccountIdByName(accountName); + if (acctId == Account.NO_ACCOUNT) return null; + return db.rawQuery(genQueryAccountMailboxes(uiProjection), + new String[] {Long.toString(acctId)}); + case UI_MESSAGES: + String id = uri.getPathSegments().get(1); + return db.rawQuery(genQueryMailboxMessages(uiProjection), new String[] {id}); + case UI_MESSAGE: + id = uri.getPathSegments().get(1); + return db.rawQuery(genQueryViewMessage(uiProjection), new String[] {id}); + } + // Not sure whether to throw an exception here, but we return null for now + return null; + } }