diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java index 745a161e0..f1fcb0dcf 100755 --- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java +++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java @@ -196,6 +196,7 @@ public abstract class EmailContent { MessageStateChange.init(); Body.initBody(); Attachment.initAttachment(); + SuggestedContact.initSuggestedContact(); } } @@ -1853,4 +1854,12 @@ public abstract class EmailContent { public static final String PROTOCOL_POLICIES_ENFORCED = "protocolPoliciesEnforced"; public static final String PROTOCOL_POLICIES_UNSUPPORTED = "protocolPoliciesUnsupported"; } + + public interface SuggestedContactColumns extends BaseColumns { + static final String ACCOUNT_KEY = "accountKey"; + static final String ADDRESS = "address"; + static final String NAME = "name"; + static final String DISPLAY_NAME = "display_name"; + static final String LAST_SEEN = "last_seen"; + } } diff --git a/emailcommon/src/com/android/emailcommon/provider/SuggestedContact.java b/emailcommon/src/com/android/emailcommon/provider/SuggestedContact.java new file mode 100644 index 000000000..ef46d9cfb --- /dev/null +++ b/emailcommon/src/com/android/emailcommon/provider/SuggestedContact.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.emailcommon.provider; + +import android.net.Uri; +import android.provider.BaseColumns; + +import com.android.emailcommon.provider.EmailContent.SuggestedContactColumns; + +/** + * A suggested contact extracted from sent and received emails to be displayed when the user + * compose a message. Tied to a specific account. + */ +public abstract class SuggestedContact extends EmailContent + implements SuggestedContactColumns { + public static final String TABLE_NAME = "SuggestedContact"; + public static Uri CONTENT_URI; + public static Uri ACCOUNT_ID_URI; + + public static final String[] PROJECTION = new String[] { + SuggestedContact._ID, + SuggestedContact.ACCOUNT_KEY, + SuggestedContact.ADDRESS, + SuggestedContact.NAME, + SuggestedContact.DISPLAY_NAME, + SuggestedContact.LAST_SEEN, + }; + + public static void initSuggestedContact() { + CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/suggestedcontact"); + ACCOUNT_ID_URI = Uri.parse(EmailContent.CONTENT_URI + "/suggestedcontact/account"); + } +} \ No newline at end of file diff --git a/provider_src/com/android/email/provider/DBHelper.java b/provider_src/com/android/email/provider/DBHelper.java index f6a0f2de3..63262a5ec 100644 --- a/provider_src/com/android/email/provider/DBHelper.java +++ b/provider_src/com/android/email/provider/DBHelper.java @@ -48,6 +48,7 @@ import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.EmailContent.MessageColumns; import com.android.emailcommon.provider.EmailContent.PolicyColumns; import com.android.emailcommon.provider.EmailContent.QuickResponseColumns; +import com.android.emailcommon.provider.EmailContent.SuggestedContactColumns; import com.android.emailcommon.provider.EmailContent.SyncColumns; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Mailbox; @@ -56,6 +57,7 @@ import com.android.emailcommon.provider.MessageMove; import com.android.emailcommon.provider.MessageStateChange; import com.android.emailcommon.provider.Policy; import com.android.emailcommon.provider.QuickResponse; +import com.android.emailcommon.provider.SuggestedContact; import com.android.emailcommon.service.LegacyPolicySet; import com.android.emailcommon.service.SyncWindow; import com.android.mail.providers.UIProvider; @@ -198,6 +200,11 @@ public final class DBHelper { // Version 101: Move body contents to external files public static final int BODY_DATABASE_VERSION = 101; + // Any changes to the database format *must* include update-in-place code. + // Original version: 1 + // Version 1: Suggested contacts + public static final int EXTRAS_DATABASE_VERSION = 1; + /* * Internal helper method for index creation. * Example: @@ -686,6 +693,24 @@ public final class DBHelper { db.execSQL(createIndex(Body.TABLE_NAME, BodyColumns.MESSAGE_KEY)); } + private static void createSuggestedContactTable(SQLiteDatabase db) { + String s = " (" + SuggestedContactColumns._ID + " integer primary key autoincrement, " + + SuggestedContactColumns.ACCOUNT_KEY + " integer, " + + SuggestedContactColumns.ADDRESS + " text, " + + SuggestedContactColumns.NAME + " text, " + + SuggestedContactColumns.DISPLAY_NAME + " text, " + + SuggestedContactColumns.LAST_SEEN + " integer" + + ");"; + db.execSQL("create table " + SuggestedContact.TABLE_NAME + s); + + // Create a unique index for account-address + String indexDDL = "create unique index " + SuggestedContact.TABLE_NAME.toLowerCase() + + "_account_address" + " on " + SuggestedContact.TABLE_NAME + + " (" + SuggestedContactColumns.ACCOUNT_KEY + ", " + + SuggestedContactColumns.ADDRESS + ");"; + db.execSQL(indexDDL); + } + private static void upgradeBodyToVersion5(final SQLiteDatabase db) { try { db.execSQL("drop table " + Body.TABLE_NAME); @@ -824,6 +849,29 @@ public final class DBHelper { } } + protected static class ExtrasDatabaseHelper extends SQLiteOpenHelper { + final Context mContext; + + ExtrasDatabaseHelper(Context context, String name) { + super(context, name, null, EXTRAS_DATABASE_VERSION); + mContext = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + LogUtils.d(TAG, "Creating EmailProviderExtras database"); + createSuggestedContactTable(db); + } + + @Override + public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { + } + + @Override + public void onOpen(SQLiteDatabase db) { + } + } + /** Counts the number of messages in each mailbox, and updates the message count column. */ @VisibleForTesting static void recalculateMessageCount(SQLiteDatabase db) { diff --git a/provider_src/com/android/email/provider/EmailProvider.java b/provider_src/com/android/email/provider/EmailProvider.java index 4bd9d4d19..ee5fe860a 100644 --- a/provider_src/com/android/email/provider/EmailProvider.java +++ b/provider_src/com/android/email/provider/EmailProvider.java @@ -42,6 +42,7 @@ import android.database.CursorWrapper; import android.database.DatabaseUtils; import android.database.MatrixCursor; import android.database.MergeCursor; +import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteStatement; @@ -100,6 +101,7 @@ import com.android.emailcommon.provider.MessageMove; import com.android.emailcommon.provider.MessageStateChange; import com.android.emailcommon.provider.Policy; import com.android.emailcommon.provider.QuickResponse; +import com.android.emailcommon.provider.SuggestedContact; import com.android.emailcommon.service.EmailServiceProxy; import com.android.emailcommon.service.EmailServiceStatus; import com.android.emailcommon.service.IEmailService; @@ -162,6 +164,7 @@ public class EmailProvider extends ContentProvider // exposed for testing public static final String DATABASE_NAME = "EmailProvider.db"; public static final String BODY_DATABASE_NAME = "EmailProviderBody.db"; + public static final String EXTRAS_DATABASE_NAME = "EmailProviderExtras.db"; // We don't back up to the backup database anymore, just keep this constant here so we can // delete the old backups and trigger a new backup to the account manager @@ -287,11 +290,15 @@ public class EmailProvider extends ContentProvider private static final int CREDENTIAL = CREDENTIAL_BASE; private static final int CREDENTIAL_ID = CREDENTIAL_BASE + 1; + private static final int SUGGESTED_CONTACT_BASE = 0xC000; + private static final int SUGGESTED_CONTACT= SUGGESTED_CONTACT_BASE; + private static final int SUGGESTED_CONTACT_ID = SUGGESTED_CONTACT_BASE + 1; + private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc. private static final SparseArray TABLE_NAMES; static { - SparseArray array = new SparseArray(11); + SparseArray array = new SparseArray(12); array.put(ACCOUNT_BASE >> BASE_SHIFT, Account.TABLE_NAME); array.put(MAILBOX_BASE >> BASE_SHIFT, Mailbox.TABLE_NAME); array.put(MESSAGE_BASE >> BASE_SHIFT, Message.TABLE_NAME); @@ -304,6 +311,7 @@ public class EmailProvider extends ContentProvider array.put(UI_BASE >> BASE_SHIFT, null); array.put(BODY_BASE >> BASE_SHIFT, Body.TABLE_NAME); array.put(CREDENTIAL_BASE >> BASE_SHIFT, Credential.TABLE_NAME); + array.put(SUGGESTED_CONTACT_BASE >> BASE_SHIFT, SuggestedContact.TABLE_NAME); TABLE_NAMES = array; } @@ -384,6 +392,7 @@ public class EmailProvider extends ContentProvider private SQLiteDatabase mDatabase; private SQLiteDatabase mBodyDatabase; + private SQLiteDatabase mExtrasDatabase; private Handler mDelayedSyncHandler; private final Set mDelayedSyncRequests = new HashSet(); @@ -494,6 +503,13 @@ public class EmailProvider extends ContentProvider String bodyFileName = mBodyDatabase.getPath(); mDatabase.execSQL("attach \"" + bodyFileName + "\" as BodyDatabase"); } + DBHelper.ExtrasDatabaseHelper extrasHelper = + new DBHelper.ExtrasDatabaseHelper(context, EXTRAS_DATABASE_NAME); + mExtrasDatabase = extrasHelper.getWritableDatabase(); + if (mExtrasDatabase != null) { + String extrasFileName = mExtrasDatabase.getPath(); + mDatabase.execSQL("attach \"" + extrasFileName + "\" as ExtrasDatabase"); + } // Restore accounts if the database is corrupted... restoreIfNeeded(context, mDatabase); @@ -575,6 +591,10 @@ public class EmailProvider extends ContentProvider mBodyDatabase.close(); mBodyDatabase = null; } + if (mExtrasDatabase != null) { + mExtrasDatabase.close(); + mExtrasDatabase = null; + } } // exposed for testing @@ -706,6 +726,7 @@ public class EmailProvider extends ContentProvider case POLICY_ID: case QUICK_RESPONSE_ID: case CREDENTIAL_ID: + case SUGGESTED_CONTACT_ID: id = uri.getPathSegments().get(1); if (match == SYNCED_MESSAGE_ID) { // For synced messages, first copy the old message to the deleted table and @@ -727,6 +748,11 @@ public class EmailProvider extends ContentProvider if (match == ACCOUNT_ID) { notifyUI(UIPROVIDER_ACCOUNT_NOTIFIER, id); notifyUI(UIPROVIDER_ALL_ACCOUNTS_NOTIFIER, null); + + // Delete account suggested contacts + mExtrasDatabase.delete(SuggestedContact.TABLE_NAME, + SuggestedContact.ACCOUNT_KEY + " = ?", new String[]{id}); + } else if (match == MAILBOX_ID) { notifyUIFolder(id, accountId); } else if (match == ATTACHMENT_ID) { @@ -750,7 +776,13 @@ public class EmailProvider extends ContentProvider case ACCOUNT: case HOSTAUTH: case POLICY: + case SUGGESTED_CONTACT: result = db.delete(tableName, selection, selectionArgs); + if (match == ACCOUNT) { + // TODO extract account deleted + // As a fallback clean all suggested contacts + mExtrasDatabase.delete(SuggestedContact.TABLE_NAME, null, null); + } break; case MESSAGE_MOVE: db.delete(MessageMove.TABLE_NAME, selection, selectionArgs); @@ -849,6 +881,10 @@ public class EmailProvider extends ContentProvider return "vnd.android.cursor.dir/email-hostauth"; case HOSTAUTH_ID: return "vnd.android.cursor.item/email-hostauth"; + case SUGGESTED_CONTACT: + return "vnd.android.cursor.item/email-suggested-contact"; + case SUGGESTED_CONTACT_ID: + return "vnd.android.cursor.dir/email-suggested-contact"; case ATTACHMENTS_CACHED_FILE_ACCESS: { SQLiteDatabase db = getDatabase(getContext()); Cursor c = db.query(Attachment.TABLE_NAME, MIME_TYPE_PROJECTION, @@ -887,7 +923,7 @@ public class EmailProvider extends ContentProvider private static Uri UIPROVIDER_RECENT_FOLDERS_NOTIFIER; @Override - public Uri insert(Uri uri, ContentValues values) { + public Uri insert(Uri uri, final ContentValues values) { Log.d(TAG, "Insert: " + uri); final int match = findMatch(uri, "insert"); final Context context = getContext(); @@ -935,6 +971,20 @@ public class EmailProvider extends ContentProvider case DELETED_MESSAGE: case MESSAGE: decodeEmailAddresses(values); + + // Update the suggested contacts of this email in the background + if (!MailPrefs.get(context).getSuggestedContactMode().equals( + MailPrefs.SuggestedContactsMode.NONE)) { + new Thread(new Runnable() { + @Override + public void run() { + if(match == MESSAGE) { + addOrUpdateSuggestedContactsFromHeaders(values); + } + } + }).start(); + } + case ATTACHMENT: case MAILBOX: case ACCOUNT: @@ -1233,14 +1283,19 @@ public class EmailProvider extends ContentProvider sURIMatcher.addURI(EmailContent.AUTHORITY, "pickSentFolder/#", ACCOUNT_PICK_SENT_FOLDER); sURIMatcher.addURI(EmailContent.AUTHORITY, "uipurgefolder/#", UI_PURGE_FOLDER); + + // Suggested Contact + sURIMatcher.addURI(EmailContent.AUTHORITY, "suggestedcontact", SUGGESTED_CONTACT); + sURIMatcher.addURI(EmailContent.AUTHORITY, "suggestedcontact/#", SUGGESTED_CONTACT_ID); } } /** - * The idea here is that the two databases (EmailProvider.db and EmailProviderBody.db must - * always be in sync (i.e. there are two database or NO databases). This code will delete - * any "orphan" database, so that both will be created together. Note that an "orphan" database - * will exist after either of the individual databases is deleted due to data corruption. + * The idea here is that the three databases (EmailProvider.db, EmailProviderBody.db + * and EmailProviderExtras.db must always be in sync (i.e. there are three database or + * NO databases). This code will delete any "orphan" database, so that both will be + * created together. Note that an "orphan" database will exist after either of the individual + * databases is deleted due to data corruption. */ public void checkDatabases() { synchronized (sDatabaseLock) { @@ -1251,18 +1306,33 @@ public class EmailProvider extends ContentProvider if (mBodyDatabase != null) { mBodyDatabase = null; } + if (mExtrasDatabase != null) { + mExtrasDatabase = null; + } // Look for orphans, and delete as necessary; these must always be in sync final File databaseFile = getContext().getDatabasePath(DATABASE_NAME); final File bodyFile = getContext().getDatabasePath(BODY_DATABASE_NAME); + final File extrasFile = getContext().getDatabasePath(BODY_DATABASE_NAME); // TODO Make sure attachments are deleted - if (databaseFile.exists() && !bodyFile.exists()) { + boolean mainDbExists = databaseFile.exists(); + boolean bodyDbExists = bodyFile.exists(); + boolean extrasDbExists = extrasFile.exists(); + boolean extrasDbShouldExists = DBHelper.EXTRAS_DATABASE_VERSION <= 1; + if (mainDbExists && (!bodyDbExists || (!extrasDbExists && extrasDbShouldExists))) { LogUtils.w(TAG, "Deleting orphaned EmailProvider database..."); getContext().deleteDatabase(DATABASE_NAME); - } else if (bodyFile.exists() && !databaseFile.exists()) { + } + if (bodyDbExists && (!mainDbExists || (!extrasDbExists && extrasDbShouldExists))) { LogUtils.w(TAG, "Deleting orphaned EmailProviderBody database..."); getContext().deleteDatabase(BODY_DATABASE_NAME); } + if (extrasDbExists && (!mainDbExists || !bodyDbExists)) { + if (DBHelper.EXTRAS_DATABASE_VERSION > 1) { + LogUtils.w(TAG, "Deleting orphaned EmailProviderExtras database..."); + getContext().deleteDatabase(EXTRAS_DATABASE_NAME); + } + } } } @@ -1291,6 +1361,7 @@ public class EmailProvider extends ContentProvider case HOSTAUTH_ID: case CREDENTIAL_ID: case POLICY_ID: + case SUGGESTED_CONTACT_ID: return new MatrixCursorWithCachedColumns(projection, 0); } } @@ -1464,6 +1535,15 @@ public class EmailProvider extends ContentProvider id = uri.getPathSegments().get(2); c = uiQuickResponseAccount(projection, id); break; + case SUGGESTED_CONTACT: + c = db.query(tableName, projection, + selection, selectionArgs, null, null, sortOrder, limit); + break; + case SUGGESTED_CONTACT_ID: + id = uri.getPathSegments().get(1); + c = db.query(tableName, projection, whereWithId(id, selection), + selectionArgs, null, null, sortOrder, limit); + break; case ATTACHMENTS_CACHED_FILE_ACCESS: if (projection == null) { projection = @@ -6089,6 +6169,85 @@ public class EmailProvider extends ContentProvider } } + /** + * This method extract the address of a new email to insert in the database + * and extract and update he suggested contact table with this addresses. + */ + private void addOrUpdateSuggestedContactsFromHeaders(ContentValues values) { + List
suggestedContacts = new ArrayList<>(); + + Long accountId = values.getAsLong(MessageColumns.ACCOUNT_KEY); + if (accountId == null) { + // Ignore the entire content. We don't have enough information to + // update the suggested contact + return; + } + + if (values.containsKey(Message.MessageColumns.TO_LIST)) { + final String to = values.getAsString(Message.MessageColumns.TO_LIST); + suggestedContacts.addAll(Arrays.asList(Address.fromHeader(to))); + } + + if (values.containsKey(Message.MessageColumns.CC_LIST)) { + final String cc = values.getAsString(Message.MessageColumns.CC_LIST); + suggestedContacts.addAll(Arrays.asList(Address.fromHeader(cc))); + } + + if (values.containsKey(Message.MessageColumns.BCC_LIST)) { + final String bcc = values.getAsString(Message.MessageColumns.BCC_LIST); + suggestedContacts.addAll(Arrays.asList(Address.fromHeader(bcc))); + } + + if (values.containsKey(Message.MessageColumns.REPLY_TO_LIST)) { + final String replyTo = values.getAsString(Message.MessageColumns.REPLY_TO_LIST); + suggestedContacts.addAll(Arrays.asList(Address.fromHeader(replyTo))); + } + + // Update or insert every suggested contact + mExtrasDatabase.beginTransactionNonExclusive(); + try { + for (Address suggestedContact : suggestedContacts) { + addOrUpdateSuggestedContact(accountId, suggestedContact); + } + mExtrasDatabase.setTransactionSuccessful(); + } finally { + mExtrasDatabase.endTransaction(); + } + } + + private void addOrUpdateSuggestedContact(long accountId, Address address) { + try { + // Update first the suggested contact, and if not exists add a new row + if (address == null) { + return; + } + + // Update + String emailAddress = address.getAddress().toLowerCase(); + String where = SuggestedContact.ACCOUNT_KEY + " = ? and " + + SuggestedContact.ADDRESS + " = ?"; + String[] args = {String.valueOf(accountId), emailAddress}; + ContentValues values = new ContentValues(); + values.put(SuggestedContact.NAME, TextUtils.isEmpty(address.getPersonal()) + ? emailAddress : address.getPersonal()); + values.put(SuggestedContact.DISPLAY_NAME, address.toString()); + values.put(SuggestedContact.LAST_SEEN, System.currentTimeMillis()); + long affectedRecords = mExtrasDatabase.update( + SuggestedContact.TABLE_NAME, values, where, args); + + // Insert + if (affectedRecords == 0) { + values.put(SuggestedContact.ACCOUNT_KEY, accountId); + values.put(SuggestedContact.ADDRESS, emailAddress); + mExtrasDatabase.insertOrThrow(SuggestedContact.TABLE_NAME, null, values); + } + + } catch (SQLException ex) { + Log.w(TAG, "Failed to insert/update suggested contact address: " + + String.valueOf(address), ex); + } + } + /** Projection used for getting email address for an account. */ private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS };