am 1b6c85b9: (-s ours) DO NOT MERGE cherry-pick of CL I0ff20aa2 from eclair-mr2
Merge commit '1b6c85b96f5cccae94d8bad6ea727accacfb7a0f' into eclair-mr2
* commit '1b6c85b96f5cccae94d8bad6ea727accacfb7a0f':
DO NOT MERGE cherry-pick of CL I0ff20aa2 from eclair-mr2
diff --git a/src/com/android/providers/contacts/ContactAggregationScheduler.java b/src/com/android/providers/contacts/ContactAggregationScheduler.java
deleted file mode 100644
index 497b6a3..0000000
--- a/src/com/android/providers/contacts/ContactAggregationScheduler.java
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source 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.providers.contacts;
-
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Process;
-import android.util.Log;
-
-/**
- * A scheduler for asynchronous aggregation of contacts. Aggregation will start after
- * a short delay after it is scheduled, unless it is scheduled again, in which case the
- * aggregation pass is delayed. There is an upper boundary on how long aggregation can
- * be delayed.
- */
-public class ContactAggregationScheduler {
-
- private static final String TAG = "ContactAggregator";
-
- public interface Aggregator {
-
- /**
- * Performs an aggregation run.
- */
- void run();
-
- /**
- * Interrupts aggregation.
- */
- void interrupt();
- }
-
- // Message ID used for communication with the aggregator
- private static final int START_AGGREGATION_MESSAGE_ID = 1;
-
- // Aggregation is delayed by this many milliseconds to allow changes to accumulate
- public static final int AGGREGATION_DELAY = 1000;
-
- // Maximum delay of aggregation from the initial aggregation request
- public static final int MAX_AGGREGATION_DELAY = 10000;
-
- // Minimum gap between requests that should cause a delay of aggregation
- public static final int DELAYED_EXECUTION_TIMEOUT = 500;
-
- public static final int STATUS_STAND_BY = 0;
- public static final int STATUS_SCHEDULED = 1;
- public static final int STATUS_RUNNING = 2;
- public static final int STATUS_INTERRUPTED = 3;
-
-
- private Aggregator mAggregator;
-
- // Aggregation status
- private int mStatus = STATUS_STAND_BY;
-
- // If true, we need to automatically reschedule aggregation after the current pass is done
- private boolean mRescheduleWhenComplete;
-
- // The time when aggregation was requested for the first time.
- // Reset when aggregation is completed
- private long mInitialRequestTimestamp;
-
- // Last time aggregation was requested
- private long mLastAggregationEndedTimestamp;
-
- private HandlerThread mHandlerThread;
- private Handler mMessageHandler;
-
- public void setAggregator(Aggregator aggregator) {
- mAggregator = aggregator;
- }
-
- public void start() {
- mHandlerThread = new HandlerThread("ContactAggregator", Process.THREAD_PRIORITY_BACKGROUND);
- mHandlerThread.start();
- mMessageHandler = new Handler(mHandlerThread.getLooper()) {
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case START_AGGREGATION_MESSAGE_ID:
- run();
- break;
-
- default:
- throw new IllegalStateException("Unhandled message: " + msg.what);
- }
- }
- };
- }
-
- public void stop() {
- mAggregator.interrupt();
- Looper looper = mHandlerThread.getLooper();
- if (looper != null) {
- looper.quit();
- }
- }
-
- /**
- * Schedules an aggregation pass after a short delay.
- */
- public synchronized void schedule() {
-
- switch (mStatus) {
- case STATUS_STAND_BY: {
-
- mInitialRequestTimestamp = currentTime();
- mStatus = STATUS_SCHEDULED;
- if (mInitialRequestTimestamp - mLastAggregationEndedTimestamp <
- DELAYED_EXECUTION_TIMEOUT) {
- runDelayed();
- } else {
- runNow();
- }
- break;
- }
-
- case STATUS_INTERRUPTED: {
-
- // If the previous aggregation run was interrupted, do not reset
- // the initial request timestamp - we don't want a continuous string
- // of interrupted runs
- mStatus = STATUS_SCHEDULED;
- runDelayed();
- break;
- }
-
- case STATUS_SCHEDULED: {
-
- // If it has been less than MAX_AGGREGATION_DELAY millis since the initial request,
- // reschedule the request.
- if (currentTime() - mInitialRequestTimestamp < MAX_AGGREGATION_DELAY) {
- runDelayed();
- }
- break;
- }
-
- case STATUS_RUNNING: {
-
- // If it has been less than MAX_AGGREGATION_DELAY millis since the initial request,
- // interrupt the current pass and reschedule the request.
- if (currentTime() - mInitialRequestTimestamp < MAX_AGGREGATION_DELAY) {
- mAggregator.interrupt();
- mStatus = STATUS_INTERRUPTED;
- }
-
- mRescheduleWhenComplete = true;
- break;
- }
- }
- }
-
- /**
- * Called just before an aggregation pass begins.
- */
- public void run() {
- synchronized (this) {
- mStatus = STATUS_RUNNING;
- mRescheduleWhenComplete = false;
- }
- try {
- mAggregator.run();
- } finally {
- mLastAggregationEndedTimestamp = currentTime();
- synchronized (this) {
- if (mStatus == STATUS_RUNNING) {
- mStatus = STATUS_STAND_BY;
- }
- if (mRescheduleWhenComplete) {
- mRescheduleWhenComplete = false;
- schedule();
- } else {
- Log.w(TAG, "No more aggregation requests");
- }
- }
- }
- }
-
- /* package */ void runNow() {
-
- // If aggregation has already been requested, cancel the previous request
- mMessageHandler.removeMessages(START_AGGREGATION_MESSAGE_ID);
-
- // Schedule aggregation for right now
- mMessageHandler.sendEmptyMessage(START_AGGREGATION_MESSAGE_ID);
- }
-
- /* package */ void runDelayed() {
-
- // If aggregation has already been requested, cancel the previous request
- mMessageHandler.removeMessages(START_AGGREGATION_MESSAGE_ID);
-
- // Schedule aggregation for AGGREGATION_DELAY milliseconds from now
- mMessageHandler.sendEmptyMessageDelayed(
- START_AGGREGATION_MESSAGE_ID, AGGREGATION_DELAY);
- }
-
- /* package */ long currentTime() {
- return System.currentTimeMillis();
- }
-}
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java
index 4475a83..8d72cc8 100644
--- a/src/com/android/providers/contacts/ContactAggregator.java
+++ b/src/com/android/providers/contacts/ContactAggregator.java
@@ -21,9 +21,9 @@
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DisplayNameSources;
-import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
+import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
@@ -36,19 +36,14 @@
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.provider.ContactsContract.AggregationExceptions;
-import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StatusUpdates;
import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.text.TextUtils;
-import android.text.util.Rfc822Token;
-import android.text.util.Rfc822Tokenizer;
import android.util.EventLog;
import android.util.Log;
@@ -63,91 +58,18 @@
* ContactAggregator deals with aggregating contact information coming from different sources.
* Two John Doe contacts from two disjoint sources are presumed to be the same
* person unless the user declares otherwise.
- * <p>
- * ContactAggregator runs on a separate thread.
*/
-public class ContactAggregator implements ContactAggregationScheduler.Aggregator {
+public class ContactAggregator {
private static final String TAG = "ContactAggregator";
- private interface DataMimetypeQuery {
+ private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
- // Data mime types used in the contact matching algorithm
- String MIMETYPE_SELECTION_IN_CLAUSE = MimetypesColumns.MIMETYPE + " IN ('"
- + Email.CONTENT_ITEM_TYPE + "','"
- + Nickname.CONTENT_ITEM_TYPE + "','"
- + Phone.CONTENT_ITEM_TYPE + "','"
- + StructuredName.CONTENT_ITEM_TYPE + "')";
-
- String[] COLUMNS = new String[] {
- MimetypesColumns.MIMETYPE, Data.DATA1
- };
-
- int MIMETYPE = 0;
- int DATA1 = 1;
- }
-
- private interface DataContactIdQuery {
- String TABLE = Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS;
-
- String[] COLUMNS = new String[] {
- Data.DATA1, RawContacts.CONTACT_ID
- };
-
- int DATA1 = 0;
- int CONTACT_ID = 1;
- }
-
- private interface NameLookupQuery {
- String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
-
- String[] COLUMNS = new String[] {
- RawContacts.CONTACT_ID, NameLookupColumns.NORMALIZED_NAME,
- NameLookupColumns.NAME_TYPE
- };
-
- int CONTACT_ID = 0;
- int NORMALIZED_NAME = 1;
- int NAME_TYPE = 2;
- }
-
- private interface EmailLookupQuery {
- String TABLE = Tables.DATA_JOIN_RAW_CONTACTS;
-
- String[] COLUMNS = new String[] {
- RawContacts.CONTACT_ID
- };
-
- int CONTACT_ID = 0;
- }
-
- private interface ContactIdQuery {
- String TABLE = Tables.CONTACTS;
-
- String[] COLUMNS = new String[] {
- Contacts._ID
- };
-
- int _ID = 0;
- }
-
- private static final String[] CONTACT_ID_COLUMN = new String[] { RawContacts._ID };
- private static final String[] CONTACT_ID_COLUMNS = new String[]{ RawContacts.CONTACT_ID };
- private static final int COL_CONTACT_ID = 0;
-
- private static final int MODE_AGGREGATION = 0;
- private static final int MODE_SUGGESTIONS = 1;
-
- /**
- * When yielding the transaction to another thread, sleep for this many milliseconds
- * to allow the other thread to build up a transaction before yielding back.
- */
- private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
-
- /**
- * The maximum number of contacts aggregated in a single transaction.
- */
- private static final int MAX_TRANSACTION_SIZE = 50;
+ private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL =
+ NameLookupColumns.NAME_TYPE + " IN ("
+ + NameLookupType.NAME_EXACT + ","
+ + NameLookupType.NAME_VARIANT + ","
+ + NameLookupType.NAME_COLLATION_KEY + ")";
// From system/core/logcat/event-log-tags
// aggregator [time, count] will be logged for each aggregator cycle.
@@ -156,10 +78,12 @@
// If we encounter more than this many contacts with matching names, aggregate only this many
private static final int PRIMARY_HIT_LIMIT = 15;
+ private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT);
// If we encounter more than this many contacts with matching phone number or email,
// don't attempt to aggregate - this is likely an error or a shared corporate data element.
private static final int SECONDARY_HIT_LIMIT = 20;
+ private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT);
// If we encounter more than this many contacts with matching name during aggregation
// suggestion lookup, ignore the remaining results.
@@ -167,12 +91,8 @@
private final ContactsProvider2 mContactsProvider;
private final ContactsDatabaseHelper mDbHelper;
- private final ContactAggregationScheduler mScheduler;
private boolean mEnabled = true;
- // Set if the current aggregation pass should be interrupted
- private volatile boolean mCancel;
-
/** Precompiled sql statement for setting an aggregated presence */
private SQLiteStatement mAggregatedPresenceReplace;
private SQLiteStatement mPresenceContactIdUpdate;
@@ -188,9 +108,25 @@
private SQLiteStatement mContactIdAndMarkAggregatedUpdate;
private SQLiteStatement mContactIdUpdate;
private SQLiteStatement mMarkAggregatedUpdate;
+ private SQLiteStatement mContactUpdate;
+ private SQLiteStatement mContactInsert;
private HashSet<Long> mRawContactsMarkedForAggregation = new HashSet<Long>();
+ private String[] mSelectionArgs1 = new String[1];
+ private String[] mSelectionArgs2 = new String[2];
+ private String[] mSelectionArgs3 = new String[3];
+ private long mMimeTypeIdEmail;
+ private long mMimeTypeIdPhoto;
+ private long mMimeTypeIdPhone;
+ private String mRawContactsQueryByRawContactId;
+ private String mRawContactsQueryByContactId;
+ private StringBuilder mSb = new StringBuilder();
+ private MatchCandidateList mCandidates = new MatchCandidateList();
+ private ContactMatcher mMatcher = new ContactMatcher();
+ private ContentValues mValues = new ContentValues();
+
+
/**
* Captures a potential match for a given name. The matching algorithm
* constructs a bunch of NameMatchCandidate objects for various potential matches
@@ -233,34 +169,11 @@
}
}
- private class AggregationNameLookupBuilder extends NameLookupBuilder {
-
- private final MatchCandidateList mCandidates;
-
- public AggregationNameLookupBuilder(NameSplitter splitter, MatchCandidateList candidates) {
- super(splitter);
- mCandidates = candidates;
- }
-
- @Override
- protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
- String name) {
- mCandidates.add(name, lookupType);
- }
-
- @Override
- protected String[] getCommonNicknameClusters(String normalizedName) {
- return mContactsProvider.getCommonNicknameClusters(normalizedName);
- }
- }
-
/**
- * Constructor. Starts a contact aggregation thread. Call {@link #quit} to kill the
- * aggregation thread. Call {@link #schedule} to kick off the aggregation process after
- * a delay of {@link ContactAggregationScheduler#AGGREGATION_DELAY} milliseconds.
+ * Constructor.
*/
public ContactAggregator(ContactsProvider2 contactsProvider,
- ContactsDatabaseHelper contactsDatabaseHelper, ContactAggregationScheduler scheduler) {
+ ContactsDatabaseHelper contactsDatabaseHelper) {
mContactsProvider = contactsProvider;
mDbHelper = contactsDatabaseHelper;
@@ -303,7 +216,7 @@
mDisplayNameUpdate = db.compileStatement(
"UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.DISPLAY_NAME + "=? " +
+ " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " +
" WHERE " + Contacts._ID + "=?");
mLookupKeyUpdate = db.compileStatement(
@@ -348,14 +261,21 @@
" SET " + PresenceColumns.CONTACT_ID + "=?" +
" WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?");
- mScheduler = scheduler;
- mScheduler.setAggregator(this);
- mScheduler.start();
+ mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL);
+ mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL);
- // Perform an aggregation pass in the beginning, which will most of the time
- // do nothing. It will only be useful if the content provider has been killed
- // before completing aggregation.
- mScheduler.schedule();
+ mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
+ mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
+ mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+
+ // Query used to retrieve data from raw contacts to populate the corresponding aggregate
+ mRawContactsQueryByRawContactId = String.format(
+ RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID,
+ mMimeTypeIdPhoto, mMimeTypeIdPhone);
+
+ mRawContactsQueryByContactId = String.format(
+ RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
+ mMimeTypeIdPhoto, mMimeTypeIdPhone);
}
public void setEnabled(boolean enabled) {
@@ -366,160 +286,17 @@
return mEnabled;
}
- /**
- * Schedules aggregation pass after a short delay. This method should be called every time
- * the {@link RawContacts#CONTACT_ID} field is reset on any record.
- */
- public void schedule() {
- if (mEnabled) {
- mScheduler.schedule();
- }
- }
-
- /**
- * Kills the contact aggregation thread.
- */
- public void quit() {
- mScheduler.stop();
- }
-
- /**
- * Invoked by the scheduler to cancel aggregation.
- */
- public void interrupt() {
- mCancel = true;
- }
-
private interface AggregationQuery {
- String TABLE = Tables.RAW_CONTACTS;
-
- String[] COLUMNS = {
- RawContacts._ID, RawContacts.CONTACT_ID
- };
+ String SQL =
+ "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts._ID + " IN(";
int _ID = 0;
int CONTACT_ID = 1;
-
- String SELECTION = RawContactsColumns.AGGREGATION_NEEDED + "=1"
- + " AND " + RawContacts.AGGREGATION_MODE
- + "=" + RawContacts.AGGREGATION_MODE_DEFAULT
- + " AND " + RawContacts.CONTACT_ID + " NOT NULL";
}
/**
- * Find all contacts that require aggregation and pass them through aggregation one by one.
- * Do not call directly. It is invoked by the scheduler.
- */
- public void run() {
- if (!mEnabled) {
- return;
- }
-
- mCancel = false;
-
- SQLiteDatabase db = mDbHelper.getWritableDatabase();
- MatchCandidateList candidates = new MatchCandidateList();
- ContactMatcher matcher = new ContactMatcher();
- ContentValues values = new ContentValues();
- long rawContactIds[] = new long[MAX_TRANSACTION_SIZE];
- long contactIds[] = new long[MAX_TRANSACTION_SIZE];
- while (!mCancel) {
- if (!aggregateBatch(db, candidates, matcher, values, rawContactIds, contactIds)) {
- break;
- }
- }
- }
-
- /**
- * Takes a batch of contacts and aggregates them. Returns the number of successfully
- * processed raw contacts.
- *
- * @return true if there are possibly more contacts to aggregate
- */
- private boolean aggregateBatch(SQLiteDatabase db, MatchCandidateList candidates,
- ContactMatcher matcher, ContentValues values, long[] rawContactIds, long[] contactIds) {
- boolean lastBatch = false;
- long elapsedTime = 0;
- int aggregatedCount = 0;
- while (!mCancel && aggregatedCount < MAX_TRANSACTION_SIZE) {
- db.beginTransaction();
- try {
-
- long start = System.currentTimeMillis();
- int count = findContactsToAggregate(db, rawContactIds, contactIds,
- MAX_TRANSACTION_SIZE - aggregatedCount);
- if (mCancel || count == 0) {
- lastBatch = true;
- break;
- }
-
- Log.i(TAG, "Contact aggregation: " + count);
- EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION,
- System.currentTimeMillis() - start, -count);
-
- for (int i = 0; i < count; i++) {
- start = System.currentTimeMillis();
- aggregateContact(db, rawContactIds[i], contactIds[i], candidates, matcher,
- values);
- long end = System.currentTimeMillis();
- elapsedTime += (end - start);
- aggregatedCount++;
- if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
-
- // We have yielded the database, so the rawContactIds and contactIds
- // arrays are no longer current - we need to refetch them
- break;
- }
- }
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, aggregatedCount);
- String performance = aggregatedCount == 0 ? "" : ", " + (elapsedTime / aggregatedCount)
- + " ms per contact";
- if (aggregatedCount != 0) {
- Log.i(TAG, "Contact aggregation complete: " + aggregatedCount + performance);
- }
-
- if (aggregatedCount > 0) {
-
- // Aggregation does not affect raw contacts and therefore there is no reason
- // to send a notification to sync adapters. We do need to notify every one else though.
- mContactsProvider.notifyChange(false);
- }
-
- return !lastBatch;
- }
-
- /**
- * Finds a batch of contacts marked for aggregation. The maximum batch size
- * is {@link #MAX_TRANSACTION_SIZE} contacts.
- * @param limit
- */
- private int findContactsToAggregate(SQLiteDatabase db, long[] rawContactIds,
- long[] contactIds, int limit) {
- Cursor c = db.query(AggregationQuery.TABLE, AggregationQuery.COLUMNS,
- AggregationQuery.SELECTION, null, null, null, null,
- String.valueOf(limit));
-
- int count = 0;
- try {
- while (c.moveToNext()) {
- rawContactIds[count] = c.getLong(AggregationQuery._ID);
- contactIds[count] = c.getLong(AggregationQuery.CONTACT_ID);
- count++;
- }
- } finally {
- c.close();
- }
- return count;
- }
-
-
- /**
* Aggregate all raw contacts that were marked for aggregation in the current transaction.
* Call just before committing the transaction.
*/
@@ -530,27 +307,33 @@
}
long start = System.currentTimeMillis();
- Log.i(TAG, "Contact aggregation: " + count);
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Contact aggregation: " + count);
+ }
+
EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -count);
+ String selectionArgs[] = new String[count];
+
+ int index = 0;
+ mSb.setLength(0);
+ mSb.append(AggregationQuery.SQL);
+ for (long rawContactId : mRawContactsMarkedForAggregation) {
+ if (index > 0) {
+ mSb.append(',');
+ }
+ mSb.append('?');
+ selectionArgs[index++] = String.valueOf(rawContactId);
+ }
+
+ mSb.append(')');
+
long rawContactIds[] = new long[count];
long contactIds[] = new long[count];
-
- StringBuilder sb = new StringBuilder();
- sb.append(RawContacts._ID + " IN(");
- for (long rawContactId : mRawContactsMarkedForAggregation) {
- sb.append(rawContactId);
- sb.append(',');
- }
-
- sb.setLength(sb.length() - 1);
- sb.append(')');
-
- Cursor c = db.query(AggregationQuery.TABLE, AggregationQuery.COLUMNS, sb.toString(), null,
- null, null, null);
-
- int index = 0;
+ Cursor c = db.rawQuery(mSb.toString(), selectionArgs);
try {
+ count = c.getCount();
+ index = 0;
while (c.moveToNext()) {
rawContactIds[index] = c.getLong(AggregationQuery._ID);
contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
@@ -560,18 +343,17 @@
c.close();
}
- MatchCandidateList candidates = new MatchCandidateList();
- ContactMatcher matcher = new ContactMatcher();
- ContentValues values = new ContentValues();
-
for (int i = 0; i < count; i++) {
- aggregateContact(db, rawContactIds[i], contactIds[i], candidates, matcher, values);
+ aggregateContact(db, rawContactIds[i], contactIds[i], mCandidates, mMatcher, mValues);
}
long elapsedTime = System.currentTimeMillis() - start;
EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, count);
- String performance = count == 0 ? "" : ", " + (elapsedTime / count) + " ms per contact";
- Log.i(TAG, "Contact aggregation complete: " + count + performance);
+
+ if (VERBOSE_LOGGING) {
+ String performance = count == 0 ? "" : ", " + (elapsedTime / count) + " ms per contact";
+ Log.i(TAG, "Contact aggregation complete: " + count + performance);
+ }
}
public void clearPendingAggregations() {
@@ -593,13 +375,10 @@
/**
* Creates a new contact based on the given raw contact. Does not perform aggregation.
*/
- public void insertContact(SQLiteDatabase db, long rawContactId) {
- ContentValues contactValues = new ContentValues();
- contactValues.put(Contacts.DISPLAY_NAME, "");
- contactValues.put(Contacts.IN_VISIBLE_GROUP, false);
- computeAggregateData(db,
- RawContactsColumns.CONCRETE_ID + "=" + rawContactId, contactValues);
- long contactId = db.insert(Tables.CONTACTS, Contacts.DISPLAY_NAME, contactValues);
+ public void onRawContactInsert(SQLiteDatabase db, long rawContactId) {
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
+ long contactId = mContactInsert.executeInsert();
setContactId(rawContactId, contactId);
mDbHelper.updateContactVisible(contactId);
}
@@ -625,9 +404,9 @@
}
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
- final ContentValues values = new ContentValues();
- computeAggregateData(db, contactId, values);
- db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+ computeAggregateData(db, contactId, mContactUpdate);
+ mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
+ mContactUpdate.execute();
mDbHelper.updateContactVisible(contactId);
updateAggregatedPresence(contactId);
@@ -675,11 +454,10 @@
markAggregated(rawContactId);
} else if (contactId == -1) {
// Splitting an aggregate
- ContentValues contactValues = new ContentValues();
- contactValues.put(RawContactsColumns.DISPLAY_NAME, "");
- computeAggregateData(db,
- RawContactsColumns.CONCRETE_ID + "=" + rawContactId, contactValues);
- contactId = db.insert(Tables.CONTACTS, Contacts.DISPLAY_NAME, contactValues);
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
+ mContactInsert);
+ contactId = mContactInsert.executeInsert();
setContactIdAndMarkAggregated(rawContactId, contactId);
mDbHelper.updateContactVisible(contactId);
@@ -702,8 +480,9 @@
}
setContactIdAndMarkAggregated(rawContactId, contactId);
- computeAggregateData(db, contactId, values);
- db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+ computeAggregateData(db, contactId, mContactUpdate);
+ mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
+ mContactUpdate.execute();
mDbHelper.updateContactVisible(contactId);
updateAggregatedPresence(contactId);
}
@@ -741,6 +520,52 @@
mPresenceContactIdUpdate.execute();
}
+ interface AggregateExceptionPrefetchQuery {
+ String TABLE = Tables.AGGREGATION_EXCEPTIONS;
+
+ String[] COLUMNS = {
+ AggregationExceptions.RAW_CONTACT_ID1,
+ AggregationExceptions.RAW_CONTACT_ID2,
+ };
+
+ int RAW_CONTACT_ID1 = 0;
+ int RAW_CONTACT_ID2 = 1;
+ }
+
+ // A set of raw contact IDs for which there are aggregation exceptions
+ private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>();
+ private boolean mAggregationExceptionIdsValid;
+
+ public void invalidateAggregationExceptionCache() {
+ mAggregationExceptionIdsValid = false;
+ }
+
+ /**
+ * Finds all raw contact IDs for which there are aggregation exceptions. The list of
+ * ids is used as an optimization in aggregation: there is no point to run a query against
+ * the agg_exceptions table if it is known that there are no records there for a given
+ * raw contact ID.
+ */
+ private void prefetchAggregationExceptionIds(SQLiteDatabase db) {
+ mAggregationExceptionIds.clear();
+ final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE,
+ AggregateExceptionPrefetchQuery.COLUMNS,
+ null, null, null, null, null);
+
+ try {
+ while (c.moveToNext()) {
+ long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1);
+ long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2);
+ mAggregationExceptionIds.add(rawContactId1);
+ mAggregationExceptionIds.add(rawContactId2);
+ }
+ } finally {
+ c.close();
+ }
+
+ mAggregationExceptionIdsValid = true;
+ }
+
interface AggregateExceptionQuery {
String TABLE = Tables.AGGREGATION_EXCEPTIONS
+ " JOIN raw_contacts raw_contacts1 "
@@ -771,6 +596,16 @@
*/
private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId,
ContactMatcher matcher) {
+ if (!mAggregationExceptionIdsValid) {
+ prefetchAggregationExceptionIds(db);
+ }
+
+ // If there are no aggregation exceptions involving this raw contact, there is no need to
+ // run a query and we can just return -1, which stands for "nothing found"
+ if (!mAggregationExceptionIds.contains(rawContactId)) {
+ return -1;
+ }
+
final Cursor c = db.query(AggregateExceptionQuery.TABLE,
AggregateExceptionQuery.COLUMNS,
AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId
@@ -825,189 +660,233 @@
private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId,
MatchCandidateList candidates, ContactMatcher matcher) {
- updateMatchScoresBasedOnDataMatches(db, rawContactId, MODE_AGGREGATION, candidates, matcher);
-
- // See if we have already found a good match based on name matches alone
- long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+ // Find good matches based on name alone
+ long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, candidates, matcher);
if (bestMatch == -1) {
// We haven't found a good match on name, see if we have any matches on phone, email etc
- bestMatch = pickBestMatchBasedOnSecondaryData(db, candidates, matcher);
+ bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher);
}
return bestMatch;
}
+
/**
* Picks the best matching contact based on secondary data matches. The method loads
* structured names for all candidate contacts and recomputes match scores using approximate
* matching.
*/
private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db,
- MatchCandidateList candidates, ContactMatcher matcher) {
+ long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates(
ContactMatcher.SCORE_THRESHOLD_PRIMARY);
if (secondaryContactIds == null || secondaryContactIds.size() > SECONDARY_HIT_LIMIT) {
return -1;
}
- StringBuilder selection = new StringBuilder();
- selection.append(RawContacts.CONTACT_ID).append(" IN (");
+ loadNameMatchCandidates(db, rawContactId, candidates, true);
+
+ mSb.setLength(0);
+ mSb.append(RawContacts.CONTACT_ID).append(" IN (");
for (int i = 0; i < secondaryContactIds.size(); i++) {
if (i != 0) {
- selection.append(',');
+ mSb.append(',');
}
- selection.append(secondaryContactIds.get(i));
+ mSb.append(secondaryContactIds.get(i));
}
- selection.append(") AND " + MimetypesColumns.MIMETYPE + "='"
- + StructuredName.CONTENT_ITEM_TYPE + "'");
- final Cursor c = db.query(DataContactIdQuery.TABLE, DataContactIdQuery.COLUMNS,
- selection.toString(), null, null, null, null);
+ // We only want to compare structured names to structured names
+ // at this stage, we need to ignore all other sources of name lookup data.
+ mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL);
- MatchCandidateList nameCandidates = new MatchCandidateList();
- AggregationNameLookupBuilder builder =
- new AggregationNameLookupBuilder(mContactsProvider.getNameSplitter(), nameCandidates);
+ matchAllCandidates(db, mSb.toString(), candidates, matcher,
+ ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null);
+
+ return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
+ }
+
+ private interface NameLookupQuery {
+ String TABLE = Tables.NAME_LOOKUP;
+
+ String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?";
+ String SELECTION_STRUCTURED_NAME_BASED =
+ SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL;
+
+ String[] COLUMNS = new String[] {
+ NameLookupColumns.NORMALIZED_NAME,
+ NameLookupColumns.NAME_TYPE
+ };
+
+ int NORMALIZED_NAME = 0;
+ int NAME_TYPE = 1;
+ }
+
+ private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId,
+ MatchCandidateList candidates, boolean structuredNameBased) {
+ candidates.clear();
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
+ structuredNameBased
+ ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED
+ : NameLookupQuery.SELECTION,
+ mSelectionArgs1, null, null, null);
try {
while (c.moveToNext()) {
- String name = c.getString(DataContactIdQuery.DATA1);
- long contactId = c.getLong(DataContactIdQuery.CONTACT_ID);
-
- nameCandidates.clear();
- builder.insertNameLookup(0, 0, name);
-
- // Note the N^2 complexity of the following fragment. This is not a huge concern
- // since the number of candidates is very small and in general secondary hits
- // in the absence of primary hits are rare.
- for (int i = 0; i < candidates.mCount; i++) {
- NameMatchCandidate candidate = candidates.mList.get(i);
-
- // We only want to compare structured names to structured names
- // at this stage, we need to ignore all other sources of name lookup data.
- if (NameLookupType.isBasedOnStructuredName(candidate.mLookupType)) {
- for (int j = 0; j < nameCandidates.mCount; j++) {
- NameMatchCandidate nameCandidate = nameCandidates.mList.get(j);
- matcher.matchName(contactId,
- nameCandidate.mLookupType, nameCandidate.mName,
- candidate.mLookupType, candidate.mName,
- ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE);
- }
- }
- }
+ String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME);
+ int type = c.getInt(NameLookupQuery.NAME_TYPE);
+ candidates.add(normalizedName, type);
}
} finally {
c.close();
}
-
- return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
}
/**
* Computes scores for contacts that have matching data rows.
*/
- private void updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
- int mode, MatchCandidateList candidates, ContactMatcher matcher) {
+ private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
+ MatchCandidateList candidates, ContactMatcher matcher) {
- final Cursor c = db.query(Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS,
- DataMimetypeQuery.COLUMNS,
- Data.RAW_CONTACT_ID + "=" + rawContactId + " AND ("
- + DataMimetypeQuery.MIMETYPE_SELECTION_IN_CLAUSE + ")",
- null, null, null, null);
+ updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+ long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+ if (bestMatch != -1) {
+ return bestMatch;
+ }
+ updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+
+ return -1;
+ }
+
+ private interface NameLookupMatchQuery {
+ String TABLE = Tables.NAME_LOOKUP + " nameA"
+ + " JOIN " + Tables.NAME_LOOKUP + " nameB" +
+ " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "="
+ + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")"
+ + " JOIN " + Tables.RAW_CONTACTS +
+ " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = "
+ + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+ String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?"
+ + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
+
+ String[] COLUMNS = new String[] {
+ RawContacts.CONTACT_ID,
+ "nameA." + NameLookupColumns.NORMALIZED_NAME,
+ "nameA." + NameLookupColumns.NAME_TYPE,
+ "nameB." + NameLookupColumns.NAME_TYPE,
+ };
+
+ int CONTACT_ID = 0;
+ int NAME = 1;
+ int NAME_TYPE_A = 2;
+ int NAME_TYPE_B = 3;
+ }
+
+ private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
+ ContactMatcher matcher) {
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS,
+ NameLookupMatchQuery.SELECTION,
+ mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING);
try {
while (c.moveToNext()) {
- String mimeType = c.getString(DataMimetypeQuery.MIMETYPE);
- String data = c.getString(DataMimetypeQuery.DATA1);
- if (mimeType.equals(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) {
- addMatchCandidatesStructuredName(data, candidates);
- } else if (mimeType.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
- if (!TextUtils.isEmpty(data)) {
- addMatchCandidatesEmail(data, mode, candidates);
- lookupEmailMatches(db, data, matcher);
- }
- } else if (mimeType.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
- if (!TextUtils.isEmpty(data)) {
- lookupPhoneMatches(db, data, matcher);
- }
- } else if (mimeType.equals(CommonDataKinds.Nickname.CONTENT_ITEM_TYPE)) {
- if (!TextUtils.isEmpty(data)) {
- addMatchCandidatesNickname(data, mode, candidates);
- lookupNicknameMatches(db, data, matcher);
- }
+ long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID);
+ String name = c.getString(NameLookupMatchQuery.NAME);
+ int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A);
+ int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B);
+ matcher.matchName(contactId, nameTypeA, name,
+ nameTypeB, name, ContactMatcher.MATCHING_ALGORITHM_EXACT);
+ if (nameTypeA == NameLookupType.NICKNAME &&
+ nameTypeB == NameLookupType.NICKNAME) {
+ matcher.updateScoreWithNicknameMatch(contactId);
}
}
} finally {
c.close();
}
-
- lookupNameMatches(db, candidates, matcher);
-
- if (mode == MODE_SUGGESTIONS) {
- lookupApproximateNameMatches(db, candidates, matcher);
- }
}
- /**
- * Looks for matches based on the full name.
- */
- private void addMatchCandidatesStructuredName(String name, MatchCandidateList candidates) {
- AggregationNameLookupBuilder builder =
- new AggregationNameLookupBuilder(mContactsProvider.getNameSplitter(), candidates);
- builder.insertNameLookup(0, 0, name);
+ private interface EmailLookupQuery {
+ String TABLE = Tables.DATA + " dataA"
+ + " JOIN " + Tables.DATA + " dataB" +
+ " ON (" + "dataA." + Email.DATA + "=dataB." + Email.DATA + ")"
+ + " JOIN " + Tables.RAW_CONTACTS +
+ " ON (dataB." + Data.RAW_CONTACT_ID + " = "
+ + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+ String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
+ + " AND dataA." + DataColumns.MIMETYPE_ID + "=?"
+ + " AND dataA." + Email.DATA + " NOT NULL"
+ + " AND dataB." + DataColumns.MIMETYPE_ID + "=?"
+ + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
+
+ String[] COLUMNS = new String[] {
+ RawContacts.CONTACT_ID
+ };
+
+ int CONTACT_ID = 0;
}
- /**
- * Extracts the user name portion from an email address and normalizes it so that it
- * can be matched against names and nicknames.
- */
- private void addMatchCandidatesEmail(String email, int mode, MatchCandidateList candidates) {
- Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
- if (tokens.length == 0) {
- return;
- }
-
- String address = tokens[0].getAddress();
- int at = address.indexOf('@');
- if (at != -1) {
- address = address.substring(0, at);
- }
-
- candidates.add(NameNormalizer.normalize(address), NameLookupType.EMAIL_BASED_NICKNAME);
- }
-
-
- /**
- * Normalizes the nickname and adds it to the list of candidates.
- */
- private void addMatchCandidatesNickname(String nickname, int mode,
- MatchCandidateList candidates) {
- candidates.add(NameNormalizer.normalize(nickname), NameLookupType.NICKNAME);
- }
-
- /**
- * Given a list of {@link NameMatchCandidate}'s, finds all matches and computes their scores.
- */
- private void lookupNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
+ private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
ContactMatcher matcher) {
+ mSelectionArgs3[0] = String.valueOf(rawContactId);
+ mSelectionArgs3[1] = mSelectionArgs3[2] = String.valueOf(mMimeTypeIdEmail);
+ Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
+ EmailLookupQuery.SELECTION,
+ mSelectionArgs3, null, null, null, SECONDARY_HIT_LIMIT_STRING);
+ try {
+ while (c.moveToNext()) {
+ long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
+ matcher.updateScoreWithEmailMatch(contactId);
+ }
+ } finally {
+ c.close();
+ }
+ }
- if (candidates.mCount == 0) {
- return;
+ private interface PhoneLookupQuery {
+ String TABLE = Tables.PHONE_LOOKUP + " phoneA"
+ + " JOIN " + Tables.DATA + " dataA"
+ + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
+ + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
+ + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
+ + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
+ + " JOIN " + Tables.DATA + " dataB"
+ + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
+ + " JOIN " + Tables.RAW_CONTACTS
+ + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
+ + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+ String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
+ + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
+ + "dataB." + Phone.NUMBER + ",?)"
+ + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
+
+ String[] COLUMNS = new String[] {
+ RawContacts.CONTACT_ID
+ };
+
+ int CONTACT_ID = 0;
+ }
+
+ private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
+ ContactMatcher matcher) {
+ mSelectionArgs2[0] = String.valueOf(rawContactId);
+ mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter();
+ Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
+ PhoneLookupQuery.SELECTION,
+ mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
+ try {
+ while (c.moveToNext()) {
+ long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
+ matcher.updateScoreWithPhoneNumberMatch(contactId);
+ }
+ } finally {
+ c.close();
}
- StringBuilder selection = new StringBuilder();
- selection.append(RawContactsColumns.AGGREGATION_NEEDED + "=0 AND ");
- selection.append(NameLookupColumns.NORMALIZED_NAME);
- selection.append(" IN (");
- for (int i = 0; i < candidates.mCount; i++) {
- DatabaseUtils.appendEscapedSQLString(selection, candidates.mList.get(i).mName);
- selection.append(",");
- }
-
- // Yank the last comma
- selection.setLength(selection.length() - 1);
- selection.append(")");
-
- matchAllCandidates(db, selection.toString(), candidates, matcher,
- ContactMatcher.MATCHING_ALGORITHM_EXACT, String.valueOf(PRIMARY_HIT_LIMIT));
}
/**
@@ -1037,22 +916,38 @@
}
}
+ private interface ContactNameLookupQuery {
+ String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
+
+ String[] COLUMNS = new String[] {
+ RawContacts.CONTACT_ID,
+ NameLookupColumns.NORMALIZED_NAME,
+ NameLookupColumns.NAME_TYPE
+ };
+
+ int CONTACT_ID = 0;
+ int NORMALIZED_NAME = 1;
+ int NAME_TYPE = 2;
+ }
+
/**
* Loads all candidate rows from the name lookup table and updates match scores based
* on that data.
*/
private void matchAllCandidates(SQLiteDatabase db, String selection,
MatchCandidateList candidates, ContactMatcher matcher, int algorithm, String limit) {
- final Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
+ final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
selection, null, null, null, null, limit);
try {
while (c.moveToNext()) {
- Long contactId = c.getLong(NameLookupQuery.CONTACT_ID);
- String name = c.getString(NameLookupQuery.NORMALIZED_NAME);
- int nameType = c.getInt(NameLookupQuery.NAME_TYPE);
+ Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
+ String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
+ int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
- // Determine which candidate produced this match
+ // Note the N^2 complexity of the following fragment. This is not a huge concern
+ // since the number of candidates is very small and in general secondary hits
+ // in the absence of primary hits are rare.
for (int i = 0; i < candidates.mCount; i++) {
NameMatchCandidate candidate = candidates.mList.get(i);
matcher.matchName(contactId, candidate.mLookupType, candidate.mName,
@@ -1064,79 +959,38 @@
}
}
- private void lookupPhoneMatches(SQLiteDatabase db, String phoneNumber, ContactMatcher matcher) {
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- mDbHelper.buildPhoneLookupAndRawContactQuery(qb, phoneNumber);
- Cursor c = qb.query(db, CONTACT_ID_COLUMNS, RawContactsColumns.AGGREGATION_NEEDED + "=0",
- null, null, null, null, String.valueOf(SECONDARY_HIT_LIMIT));
- try {
- while (c.moveToNext()) {
- long contactId = c.getLong(COL_CONTACT_ID);
- matcher.updateScoreWithPhoneNumberMatch(contactId);
- }
- } finally {
- c.close();
- }
- }
-
- /**
- * Finds exact email matches and updates their match scores.
- */
- private void lookupEmailMatches(SQLiteDatabase db, String address, ContactMatcher matcher) {
- long mimetypeId = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
- Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
- DataColumns.MIMETYPE_ID + "=" + mimetypeId
- + " AND " + Email.DATA + "=?"
- + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0",
- new String[] {address}, null, null, null, String.valueOf(SECONDARY_HIT_LIMIT));
- try {
- while (c.moveToNext()) {
- long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
- matcher.updateScoreWithEmailMatch(contactId);
- }
- } finally {
- c.close();
- }
- }
-
- /**
- * Finds exact nickname matches in the name lookup table and updates their match scores.
- */
- private void lookupNicknameMatches(SQLiteDatabase db, String nickname, ContactMatcher matcher) {
- String normalized = NameNormalizer.normalize(nickname);
- Cursor c = db.query(true, Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS, CONTACT_ID_COLUMNS,
- NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NICKNAME + " AND "
- + NameLookupColumns.NORMALIZED_NAME + "='" + normalized + "' AND "
- + RawContactsColumns.AGGREGATION_NEEDED + "=0",
- null, null, null, null, null);
- try {
- while (c.moveToNext()) {
- long contactId = c.getLong(COL_CONTACT_ID);
- matcher.updateScoreWithNicknameMatch(contactId);
- }
- } finally {
- c.close();
- }
- }
-
private interface RawContactsQuery {
- String[] COLUMNS = new String[] {
- RawContactsColumns.CONCRETE_ID,
- RawContactsColumns.DISPLAY_NAME,
- RawContactsColumns.DISPLAY_NAME_SOURCE,
- RawContacts.ACCOUNT_TYPE,
- RawContacts.ACCOUNT_NAME,
- RawContacts.SOURCE_ID,
- RawContacts.CUSTOM_RINGTONE,
- RawContacts.SEND_TO_VOICEMAIL,
- RawContacts.LAST_TIME_CONTACTED,
- RawContacts.TIMES_CONTACTED,
- RawContacts.STARRED,
- RawContacts.IS_RESTRICTED,
- DataColumns.CONCRETE_ID,
- DataColumns.CONCRETE_MIMETYPE_ID,
- Data.IS_SUPER_PRIMARY,
- };
+ String SQL_FORMAT =
+ "SELECT "
+ + RawContactsColumns.CONCRETE_ID + ","
+ + RawContactsColumns.DISPLAY_NAME + ","
+ + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
+ + RawContacts.ACCOUNT_TYPE + ","
+ + RawContacts.ACCOUNT_NAME + ","
+ + RawContacts.SOURCE_ID + ","
+ + RawContacts.CUSTOM_RINGTONE + ","
+ + RawContacts.SEND_TO_VOICEMAIL + ","
+ + RawContacts.LAST_TIME_CONTACTED + ","
+ + RawContacts.TIMES_CONTACTED + ","
+ + RawContacts.STARRED + ","
+ + RawContacts.IS_RESTRICTED + ","
+ + DataColumns.CONCRETE_ID + ","
+ + DataColumns.CONCRETE_MIMETYPE_ID + ","
+ + Data.IS_SUPER_PRIMARY +
+ " FROM " + Tables.RAW_CONTACTS +
+ " LEFT OUTER JOIN " + Tables.DATA +
+ " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
+ + " AND ((" + DataColumns.MIMETYPE_ID + "=%d"
+ + " AND " + Photo.PHOTO + " NOT NULL)"
+ + " OR (" + DataColumns.MIMETYPE_ID + "=%d"
+ + " AND " + Phone.NUMBER + " NOT NULL)))";
+
+ String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT +
+ " WHERE " + RawContactsColumns.CONCRETE_ID + "=?";
+
+ String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT +
+ " WHERE " + RawContacts.CONTACT_ID + "=?"
+ + " AND " + RawContacts.DELETED + "=0";
int RAW_CONTACT_ID = 0;
int DISPLAY_NAME = 1;
@@ -1155,23 +1009,69 @@
int IS_SUPER_PRIMARY = 14;
}
+ private interface ContactReplaceSqlStatement {
+ String UPDATE_SQL =
+ "UPDATE " + Tables.CONTACTS +
+ " SET "
+ + Contacts.NAME_RAW_CONTACT_ID + "=?, "
+ + Contacts.PHOTO_ID + "=?, "
+ + Contacts.SEND_TO_VOICEMAIL + "=?, "
+ + Contacts.CUSTOM_RINGTONE + "=?, "
+ + Contacts.LAST_TIME_CONTACTED + "=?, "
+ + Contacts.TIMES_CONTACTED + "=?, "
+ + Contacts.STARRED + "=?, "
+ + Contacts.HAS_PHONE_NUMBER + "=?, "
+ + ContactsColumns.SINGLE_IS_RESTRICTED + "=?, "
+ + Contacts.LOOKUP_KEY + "=? " +
+ " WHERE " + Contacts._ID + "=?";
+
+ String INSERT_SQL =
+ "INSERT INTO " + Tables.CONTACTS + " ("
+ + Contacts.NAME_RAW_CONTACT_ID + ", "
+ + Contacts.PHOTO_ID + ", "
+ + Contacts.SEND_TO_VOICEMAIL + ", "
+ + Contacts.CUSTOM_RINGTONE + ", "
+ + Contacts.LAST_TIME_CONTACTED + ", "
+ + Contacts.TIMES_CONTACTED + ", "
+ + Contacts.STARRED + ", "
+ + Contacts.HAS_PHONE_NUMBER + ", "
+ + ContactsColumns.SINGLE_IS_RESTRICTED + ", "
+ + Contacts.LOOKUP_KEY + ", "
+ + Contacts.IN_VISIBLE_GROUP + ") " +
+ " VALUES (?,?,?,?,?,?,?,?,?,?,0)";
+
+ int NAME_RAW_CONTACT_ID = 1;
+ int PHOTO_ID = 2;
+ int SEND_TO_VOICEMAIL = 3;
+ int CUSTOM_RINGTONE = 4;
+ int LAST_TIME_CONTACTED = 5;
+ int TIMES_CONTACTED = 6;
+ int STARRED = 7;
+ int HAS_PHONE_NUMBER = 8;
+ int SINGLE_IS_RESTRICTED = 9;
+ int LOOKUP_KEY = 10;
+ int CONTACT_ID = 11;
+ }
+
/**
* Computes aggregate-level data for the specified aggregate contact ID.
*/
- private void computeAggregateData(SQLiteDatabase db, long contactId, ContentValues values) {
- computeAggregateData(db, RawContacts.CONTACT_ID + "=" + contactId
- + " AND " + RawContacts.DELETED + "=0", values);
+ private void computeAggregateData(SQLiteDatabase db, long contactId,
+ SQLiteStatement statement) {
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement);
}
/**
* Computes aggregate-level data from constituent raw contacts.
*/
- private void computeAggregateData(final SQLiteDatabase db, String selection,
- final ContentValues values) {
+ private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs,
+ SQLiteStatement statement) {
long currentRawContactId = -1;
int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
String bestDisplayName = null;
long bestPhotoId = -1;
+ long bestNameRawContactId = -1;
boolean foundSuperPrimaryPhoto = false;
String photoAccount = null;
int totalRowCount = 0;
@@ -1179,25 +1079,12 @@
String contactCustomRingtone = null;
long contactLastTimeContacted = 0;
int contactTimesContacted = 0;
- boolean contactStarred = false;
+ int contactStarred = 0;
int singleIsRestricted = 1;
int hasPhoneNumber = 0;
- StringBuilder lookupKey = new StringBuilder();
- long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
- long phoneMimeType = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
-
- String isPhotoSql = "(" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
- + Photo.PHOTO + " NOT NULL)";
- String isPhoneSql = "(" + DataColumns.MIMETYPE_ID + "=" + phoneMimeType + " AND "
- + Phone.NUMBER + " NOT NULL)";
-
- String tables = Tables.RAW_CONTACTS + " LEFT OUTER JOIN " + Tables.DATA + " ON("
- + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
- + " AND (" + isPhotoSql + " OR " + isPhoneSql + "))";
-
- final Cursor c = db.query(tables, RawContactsQuery.COLUMNS, selection, null, null, null,
- null);
+ mSb.setLength(0); // Lookup key
+ Cursor c = db.rawQuery(sql, sqlArgs);
try {
while (c.moveToNext()) {
long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
@@ -1212,17 +1099,24 @@
if (bestDisplayName == null) {
bestDisplayName = displayName;
bestDisplayNameSource = displayNameSource;
+ bestNameRawContactId = rawContactId;
} else if (bestDisplayNameSource != displayNameSource) {
if (bestDisplayNameSource < displayNameSource) {
bestDisplayName = displayName;
bestDisplayNameSource = displayNameSource;
+ bestNameRawContactId = rawContactId;
}
} else if (NameNormalizer.compareComplexity(displayName,
bestDisplayName) > 0) {
bestDisplayName = displayName;
+ bestNameRawContactId = rawContactId;
}
}
+ if (bestNameRawContactId == -1) {
+ bestNameRawContactId = rawContactId;
+ }
+
// Contact options
if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
boolean sendToVoicemail =
@@ -1247,7 +1141,9 @@
contactTimesContacted = timesContacted;
}
- contactStarred |= (c.getInt(RawContactsQuery.STARRED) != 0);
+ if (c.getInt(RawContactsQuery.STARRED) != 0) {
+ contactStarred = 1;
+ }
// Single restricted
if (totalRowCount > 1) {
@@ -1262,7 +1158,7 @@
}
}
- ContactLookupKey.appendToLookupKey(lookupKey,
+ ContactLookupKey.appendToLookupKey(mSb,
c.getString(RawContactsQuery.ACCOUNT_TYPE),
c.getString(RawContactsQuery.ACCOUNT_NAME),
c.getString(RawContactsQuery.SOURCE_ID),
@@ -1273,7 +1169,7 @@
long dataId = c.getLong(RawContactsQuery.DATA_ID);
int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
boolean superprimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0;
- if (mimetypeId == photoMimeType) {
+ if (mimetypeId == mMimeTypeIdPhoto) {
// For now, just choose the first photo in a list sorted by account name.
String account = c.getString(RawContactsQuery.ACCOUNT_NAME);
@@ -1285,7 +1181,7 @@
bestPhotoId = dataId;
foundSuperPrimaryPhoto |= superprimary;
}
- } else if (mimetypeId == phoneMimeType) {
+ } else if (mimetypeId == mMimeTypeIdPhone) {
hasPhoneNumber = 1;
}
}
@@ -1295,28 +1191,30 @@
c.close();
}
- values.clear();
-
- // If don't have anything to base the display name on, let's just leave what was in
- // that field hoping that there was something there before and it is still valid.
- if (bestDisplayName != null) {
- values.put(Contacts.DISPLAY_NAME, bestDisplayName);
- }
+ statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID, bestNameRawContactId);
if (bestPhotoId != -1) {
- values.put(Contacts.PHOTO_ID, bestPhotoId);
+ statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId);
} else {
- values.putNull(Contacts.PHOTO_ID);
+ statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID);
}
- values.put(Contacts.SEND_TO_VOICEMAIL, totalRowCount == contactSendToVoicemail);
- values.put(Contacts.CUSTOM_RINGTONE, contactCustomRingtone);
- values.put(Contacts.LAST_TIME_CONTACTED, contactLastTimeContacted);
- values.put(Contacts.TIMES_CONTACTED, contactTimesContacted);
- values.put(Contacts.STARRED, contactStarred);
- values.put(Contacts.HAS_PHONE_NUMBER, hasPhoneNumber);
- values.put(ContactsColumns.SINGLE_IS_RESTRICTED, singleIsRestricted);
- values.put(Contacts.LOOKUP_KEY, Uri.encode(lookupKey.toString()));
+ statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL,
+ totalRowCount == contactSendToVoicemail ? 1 : 0);
+ DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
+ contactCustomRingtone);
+ statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED,
+ contactLastTimeContacted);
+ statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED,
+ contactTimesContacted);
+ statement.bindLong(ContactReplaceSqlStatement.STARRED,
+ contactStarred);
+ statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER,
+ hasPhoneNumber);
+ statement.bindLong(ContactReplaceSqlStatement.SINGLE_IS_RESTRICTED,
+ singleIsRestricted);
+ statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
+ Uri.encode(mSb.toString()));
}
private interface PhotoIdQuery {
@@ -1348,8 +1246,9 @@
+ " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
+ Photo.PHOTO + " NOT NULL))";
+ mSelectionArgs1[0] = String.valueOf(contactId);
final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS,
- RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+ RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
try {
while (c.moveToNext()) {
long dataId = c.getLong(PhotoIdQuery.DATA_ID);
@@ -1386,57 +1285,68 @@
private interface DisplayNameQuery {
String[] COLUMNS = new String[] {
+ RawContacts._ID,
RawContactsColumns.DISPLAY_NAME,
RawContactsColumns.DISPLAY_NAME_SOURCE,
};
- int DISPLAY_NAME = 0;
- int DISPLAY_NAME_SOURCE = 1;
+ int _ID = 0;
+ int DISPLAY_NAME = 1;
+ int DISPLAY_NAME_SOURCE = 2;
}
- public void updateDisplayName(SQLiteDatabase db, long rawContactId) {
-
+ public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) {
long contactId = mDbHelper.getContactId(rawContactId);
if (contactId == 0) {
return;
}
+ updateDisplayNameForContact(db, contactId);
+ }
+
+ public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) {
int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
String bestDisplayName = null;
+ long bestNameRawContactId = -1;
-
+ mSelectionArgs1[0] = String.valueOf(contactId);
final Cursor c = db.query(Tables.RAW_CONTACTS, DisplayNameQuery.COLUMNS,
- RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+ RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
try {
while (c.moveToNext()) {
+ long id = c.getLong(DisplayNameQuery._ID);
String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME);
int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE);
if (!TextUtils.isEmpty(displayName)) {
if (bestDisplayName == null) {
bestDisplayName = displayName;
bestDisplayNameSource = displayNameSource;
+ bestNameRawContactId = id;
} else if (bestDisplayNameSource != displayNameSource) {
if (bestDisplayNameSource < displayNameSource) {
bestDisplayName = displayName;
bestDisplayNameSource = displayNameSource;
+ bestNameRawContactId = id;
}
} else if (NameNormalizer.compareComplexity(displayName,
bestDisplayName) > 0) {
bestDisplayName = displayName;
+ bestNameRawContactId = id;
}
}
+ if (bestNameRawContactId == -1) {
+ bestNameRawContactId = id;
+ }
}
} finally {
c.close();
}
- if (bestDisplayName == null) {
- mDisplayNameUpdate.bindNull(1);
- } else {
- mDisplayNameUpdate.bindString(1, bestDisplayName);
+ if (bestNameRawContactId != -1) {
+ mDisplayNameUpdate.bindLong(1, bestNameRawContactId);
+ mDisplayNameUpdate.bindLong(2, contactId);
+ mDisplayNameUpdate.execute();
}
- mDisplayNameUpdate.bindLong(2, contactId);
- mDisplayNameUpdate.execute();
}
/**
@@ -1476,12 +1386,13 @@
return;
}
- StringBuilder lookupKey = new StringBuilder();
+ mSb.setLength(0);
+ mSelectionArgs1[0] = String.valueOf(contactId);
final Cursor c = db.query(Tables.RAW_CONTACTS, LookupKeyQuery.COLUMNS,
- RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+ RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
try {
while (c.moveToNext()) {
- ContactLookupKey.appendToLookupKey(lookupKey,
+ ContactLookupKey.appendToLookupKey(mSb,
c.getString(LookupKeyQuery.ACCOUNT_TYPE),
c.getString(LookupKeyQuery.ACCOUNT_NAME),
c.getString(LookupKeyQuery.SOURCE_ID),
@@ -1491,10 +1402,10 @@
c.close();
}
- if (lookupKey.length() == 0) {
+ if (mSb.length() == 0) {
mLookupKeyUpdate.bindNull(1);
} else {
- mLookupKeyUpdate.bindString(1, lookupKey.toString());
+ mLookupKeyUpdate.bindString(1, mSb.toString());
}
mLookupKeyUpdate.bindLong(2, contactId);
mLookupKeyUpdate.execute();
@@ -1526,6 +1437,14 @@
filter);
}
+ private interface ContactIdQuery {
+ String[] COLUMNS = new String[] {
+ Contacts._ID
+ };
+
+ int _ID = 0;
+ }
+
/**
* Loads contacts with specified IDs and returns them in the order of IDs in the
* supplied list.
@@ -1610,6 +1529,16 @@
return new ReorderingCursorWrapper(cursor, positionMap);
}
+ private interface RawContactIdQuery {
+ String TABLE = Tables.RAW_CONTACTS;
+
+ String[] COLUMNS = new String[] {
+ RawContacts._ID
+ };
+
+ int _ID = 0;
+ }
+
/**
* Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
* descending order of match score.
@@ -1622,12 +1551,12 @@
// Don't aggregate a contact with itself
matcher.keepOut(contactId);
- final Cursor c = db.query(Tables.RAW_CONTACTS, CONTACT_ID_COLUMN,
+ final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
try {
while (c.moveToNext()) {
- long rawContactId = c.getLong(0);
- updateMatchScoresBasedOnDataMatches(db, rawContactId, MODE_SUGGESTIONS, candidates,
+ long rawContactId = c.getLong(RawContactIdQuery._ID);
+ updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
matcher);
}
} finally {
@@ -1636,4 +1565,17 @@
return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST);
}
+
+ /**
+ * Computes scores for contacts that have matching data rows.
+ */
+ private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
+ long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
+
+ updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+ loadNameMatchCandidates(db, rawContactId, candidates, false);
+ lookupApproximateNameMatches(db, candidates, matcher);
+ }
}
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index f248116..740ade6 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -25,6 +25,7 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
+import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
@@ -49,6 +50,7 @@
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.SocialContract.Activities;
import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
import android.util.Log;
import java.util.HashMap;
@@ -61,11 +63,14 @@
/* package */ class ContactsDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "ContactsDatabaseHelper";
- private static final int DATABASE_VERSION = 105;
+ private static final int DATABASE_VERSION = 203;
private static final String DATABASE_NAME = "contacts2.db";
private static final String DATABASE_PRESENCE = "presence_db";
+ /** size of the compiled-sql statement cache mainatained by {@link SQLiteDatabase} */
+ private static final int MAX_CACHE_SIZE_FOR_CONTACTS_DB = 250;
+
public interface Tables {
public static final String CONTACTS = "contacts";
public static final String RAW_CONTACTS = "raw_contacts";
@@ -181,7 +186,7 @@
+ "LEFT OUTER JOIN packages ON (activities.package_id = packages._id) "
+ "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id) "
+ "LEFT OUTER JOIN raw_contacts ON (activities.author_contact_id = " +
- "raw_contacts._id) "
+ "raw_contacts._id) "
+ "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
public static final String NAME_LOOKUP_JOIN_RAW_CONTACTS = "name_lookup "
@@ -251,8 +256,6 @@
public static final String LAST_STATUS_UPDATE_ID = "status_update_id";
public static final String CONCRETE_ID = Tables.CONTACTS + "." + BaseColumns._ID;
- public static final String CONCRETE_DISPLAY_NAME = Tables.CONTACTS + "."
- + Contacts.DISPLAY_NAME;
public static final String CONCRETE_TIMES_CONTACTED = Tables.CONTACTS + "."
+ Contacts.TIMES_CONTACTED;
@@ -296,6 +299,12 @@
public static final String DISPLAY_NAME = "display_name";
public static final String DISPLAY_NAME_SOURCE = "display_name_source";
public static final String AGGREGATION_NEEDED = "aggregation_needed";
+ public static final String CONTACT_IN_VISIBLE_GROUP = "contact_in_visible_group";
+
+ public static final String CONCRETE_DISPLAY_NAME =
+ Tables.RAW_CONTACTS + "." + DISPLAY_NAME;
+ public static final String CONCRETE_CONTACT_ID =
+ Tables.RAW_CONTACTS + "." + RawContacts.CONTACT_ID;
}
/**
@@ -376,6 +385,7 @@
public static final String DATA_ID = "data_id";
public static final String RAW_CONTACT_ID = "raw_contact_id";
public static final String NORMALIZED_NUMBER = "normalized_number";
+ public static final String MIN_MATCH = "min_match";
}
public interface NameLookupColumns {
@@ -496,12 +506,14 @@
/** Compiled statements for updating {@link Contacts#IN_VISIBLE_GROUP}. */
private SQLiteStatement mVisibleUpdate;
private SQLiteStatement mVisibleSpecificUpdate;
+ private SQLiteStatement mVisibleUpdateRawContacts;
+ private SQLiteStatement mVisibleSpecificUpdateRawContacts;
private boolean mReopenDatabase = false;
private static ContactsDatabaseHelper sSingleton = null;
- private boolean mUseStrictPhoneNumberComparation;
+ private boolean mUseStrictPhoneNumberComparison;
/**
* List of package names with access to {@link RawContacts#IS_RESTRICTED} data.
@@ -526,7 +538,7 @@
mContext = context;
mSyncState = new SyncStateContentProviderHelper();
- mUseStrictPhoneNumberComparation =
+ mUseStrictPhoneNumberComparison =
resources.getBoolean(
com.android.internal.R.bool.config_use_strict_phone_number_comparation);
int resourceId = resources.getIdentifier("unrestricted_packages", "array",
@@ -570,6 +582,18 @@
mVisibleSpecificUpdate = db.compileStatement(visibleUpdate + " WHERE "
+ ContactsColumns.CONCRETE_ID + "=?");
+ String visibleUpdateRawContacts =
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" +
+ "SELECT " + Contacts.IN_VISIBLE_GROUP +
+ " FROM " + Tables.CONTACTS +
+ " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")" +
+ " WHERE " + RawContacts.DELETED + "=0";
+
+ mVisibleUpdateRawContacts = db.compileStatement(visibleUpdateRawContacts);
+ mVisibleSpecificUpdateRawContacts = db.compileStatement(visibleUpdateRawContacts +
+ " AND " + RawContacts.CONTACT_ID + "=?");
+
db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";");
db.execSQL("CREATE TABLE IF NOT EXISTS " + DATABASE_PRESENCE + "." + Tables.PRESENCE + " ("+
StatusUpdates.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," +
@@ -645,7 +669,7 @@
// One row per group of contacts corresponding to the same person
db.execSQL("CREATE TABLE " + Tables.CONTACTS + " (" +
BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Contacts.DISPLAY_NAME + " TEXT," +
+ Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
Contacts.PHOTO_ID + " INTEGER REFERENCES data(_id)," +
Contacts.CUSTOM_RINGTONE + " TEXT," +
Contacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
@@ -660,8 +684,7 @@
");");
db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" +
- Contacts.IN_VISIBLE_GROUP + "," +
- Contacts.DISPLAY_NAME + " COLLATE LOCALIZED" +
+ Contacts.IN_VISIBLE_GROUP +
");");
db.execSQL("CREATE INDEX contacts_has_phone_index ON " + Tables.CONTACTS + " (" +
@@ -672,6 +695,10 @@
ContactsColumns.SINGLE_IS_RESTRICTED +
");");
+ db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" +
+ Contacts.NAME_RAW_CONTACT_ID +
+ ");");
+
// Contacts table
db.execSQL("CREATE TABLE " + Tables.RAW_CONTACTS + " (" +
RawContacts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -694,6 +721,7 @@
RawContactsColumns.DISPLAY_NAME + " TEXT," +
RawContactsColumns.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " +
DisplayNameSources.UNDEFINED + "," +
+ RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 0," +
RawContacts.SYNC1 + " TEXT, " +
RawContacts.SYNC2 + " TEXT, " +
RawContacts.SYNC3 + " TEXT, " +
@@ -724,6 +752,11 @@
// RawContactsColumns.AGGREGATION_NEEDED +
// ");");
+ db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
+ RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+ RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" +
+ ");");
+
// Package name mapping table
db.execSQL("CREATE TABLE " + Tables.PACKAGES + " (" +
PackagesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -784,7 +817,8 @@
+ " INTEGER PRIMARY KEY REFERENCES data(_id) NOT NULL," +
PhoneLookupColumns.RAW_CONTACT_ID
+ " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
- PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL" +
+ PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL," +
+ PhoneLookupColumns.MIN_MATCH + " TEXT NOT NULL" +
");");
db.execSQL("CREATE INDEX phone_lookup_index ON " + Tables.PHONE_LOOKUP + " (" +
@@ -793,6 +827,12 @@
PhoneLookupColumns.DATA_ID +
");");
+ db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
+ PhoneLookupColumns.MIN_MATCH + "," +
+ PhoneLookupColumns.RAW_CONTACT_ID + "," +
+ PhoneLookupColumns.DATA_ID +
+ ");");
+
// Private name/nickname table used for lookup
db.execSQL("CREATE TABLE " + Tables.NAME_LOOKUP + " (" +
NameLookupColumns.DATA_ID
@@ -991,7 +1031,7 @@
*/
db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_marked_deleted;");
db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_marked_deleted "
- + " BEFORE UPDATE ON " + Tables.RAW_CONTACTS
+ + " AFTER UPDATE ON " + Tables.RAW_CONTACTS
+ " BEGIN "
+ " UPDATE " + Tables.RAW_CONTACTS
+ " SET "
@@ -1001,7 +1041,7 @@
+ " END");
db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_updated;");
- db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated BEFORE UPDATE ON " + Tables.DATA
+ db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated AFTER UPDATE ON " + Tables.DATA
+ " BEGIN "
+ " UPDATE " + Tables.DATA
+ " SET " + Data.DATA_VERSION + "=OLD." + Data.DATA_VERSION + "+1 "
@@ -1028,7 +1068,7 @@
db.execSQL("DROP TRIGGER IF EXISTS " + Tables.GROUPS + "_updated1;");
db.execSQL("CREATE TRIGGER " + Tables.GROUPS + "_updated1 "
- + " BEFORE UPDATE ON " + Tables.GROUPS
+ + " AFTER UPDATE ON " + Tables.GROUPS
+ " BEGIN "
+ " UPDATE " + Tables.GROUPS
+ " SET "
@@ -1097,14 +1137,16 @@
String dataSelect = "SELECT "
+ DataColumns.CONCRETE_ID + " AS " + Data._ID + ","
+ Data.RAW_CONTACT_ID + ", "
- + RawContacts.CONTACT_ID + ", "
+ + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
+ syncColumns + ", "
+ dataColumns + ", "
+ contactOptionColumns + ", "
- + ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + Contacts.DISPLAY_NAME + ", "
+ + "name_raw_contact." + RawContactsColumns.DISPLAY_NAME
+ + " AS " + Contacts.DISPLAY_NAME + ", "
+ + "name_raw_contact." + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
+ + " AS " + Contacts.IN_VISIBLE_GROUP + ", "
+ Contacts.LOOKUP_KEY + ", "
+ Contacts.PHOTO_ID + ", "
- + Contacts.IN_VISIBLE_GROUP + ", "
+ ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
+ Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
+ " FROM " + Tables.DATA
@@ -1113,7 +1155,9 @@
+ " JOIN " + Tables.RAW_CONTACTS + " ON ("
+ DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
+ " JOIN " + Tables.CONTACTS + " ON ("
- + RawContacts.CONTACT_ID + "=" + Tables.CONTACTS + "." + Contacts._ID + ")"
+ + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"
+ + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
+ + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"
+ " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
+ DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
+ " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
@@ -1123,7 +1167,7 @@
db.execSQL("CREATE VIEW " + Views.DATA_ALL + " AS " + dataSelect);
db.execSQL("CREATE VIEW " + Views.DATA_RESTRICTED + " AS " + dataSelect + " WHERE "
- + RawContacts.IS_RESTRICTED + "=0");
+ + RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0");
String rawContactOptionColumns =
RawContacts.CUSTOM_RINGTONE + ","
@@ -1148,13 +1192,13 @@
String contactsColumns =
ContactsColumns.CONCRETE_CUSTOM_RINGTONE
+ " AS " + Contacts.CUSTOM_RINGTONE + ", "
- + ContactsColumns.CONCRETE_DISPLAY_NAME
+ + "name_raw_contact." + RawContactsColumns.DISPLAY_NAME
+ " AS " + Contacts.DISPLAY_NAME + ", "
- + Contacts.IN_VISIBLE_GROUP + ", "
+ + "name_raw_contact." + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
+ + " AS " + Contacts.IN_VISIBLE_GROUP + ", "
+ Contacts.HAS_PHONE_NUMBER + ", "
+ Contacts.LOOKUP_KEY + ", "
+ Contacts.PHOTO_ID + ", "
- + Contacts.IN_VISIBLE_GROUP + ", "
+ ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
+ " AS " + Contacts.LAST_TIME_CONTACTED + ", "
+ ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
@@ -1168,16 +1212,13 @@
String contactsSelect = "SELECT "
+ ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
+ contactsColumns
- + " FROM " + Tables.CONTACTS;
-
- String restrictedContactsSelect = "SELECT "
- + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
- + contactsColumns
+ " FROM " + Tables.CONTACTS
- + " WHERE " + ContactsColumns.SINGLE_IS_RESTRICTED + "=0";
+ + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
+ + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
db.execSQL("CREATE VIEW " + Views.CONTACTS_ALL + " AS " + contactsSelect);
- db.execSQL("CREATE VIEW " + Views.CONTACTS_RESTRICTED + " AS " + restrictedContactsSelect);
+ db.execSQL("CREATE VIEW " + Views.CONTACTS_RESTRICTED + " AS " + contactsSelect
+ + " WHERE " + ContactsColumns.SINGLE_IS_RESTRICTED + "=0");
}
private static void createGroupsView(SQLiteDatabase db) {
@@ -1336,18 +1377,133 @@
oldVersion++;
}
- if (oldVersion == 104) {
+ if (oldVersion == 104 || oldVersion == 201) {
LegacyApiSupport.createViews(db);
LegacyApiSupport.createSettingsTable(db);
oldVersion++;
}
+ if (oldVersion == 105) {
+ addColumnPhoneNumberMinMatch(db);
+ oldVersion = 202;
+ }
+
+ if (oldVersion == 202) {
+ addNameRawContactIdColumn(db);
+ createContactsViews(db);
+ oldVersion++;
+ }
+
if (oldVersion != newVersion) {
throw new IllegalStateException(
"error upgrading the database to version " + newVersion);
}
}
+ private void addColumnPhoneNumberMinMatch(SQLiteDatabase db) {
+ db.execSQL(
+ "ALTER TABLE " + Tables.PHONE_LOOKUP +
+ " ADD " + PhoneLookupColumns.MIN_MATCH + " TEXT;");
+
+ db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
+ PhoneLookupColumns.MIN_MATCH + "," +
+ PhoneLookupColumns.RAW_CONTACT_ID + "," +
+ PhoneLookupColumns.DATA_ID +
+ ");");
+
+ updateIndexStats(db, Tables.PHONE_LOOKUP,
+ "phone_lookup_min_match_index", "10000 2 2 1");
+
+ SQLiteStatement update = db.compileStatement(
+ "UPDATE " + Tables.PHONE_LOOKUP +
+ " SET " + PhoneLookupColumns.MIN_MATCH + "=?" +
+ " WHERE " + PhoneLookupColumns.DATA_ID + "=?");
+
+ // Populate the new column
+ Cursor c = db.query(Tables.PHONE_LOOKUP + " JOIN " + Tables.DATA +
+ " ON (" + PhoneLookupColumns.DATA_ID + "=" + DataColumns.CONCRETE_ID + ")",
+ new String[]{Data._ID, Phone.NUMBER}, null, null, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long dataId = c.getLong(0);
+ String number = c.getString(1);
+ if (!TextUtils.isEmpty(number)) {
+ update.bindString(1, PhoneNumberUtils.toCallerIDMinMatch(number));
+ update.bindLong(2, dataId);
+ update.execute();
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ private static void addNameRawContactIdColumn(SQLiteDatabase db) {
+ db.execSQL(
+ "ALTER TABLE " + Tables.CONTACTS +
+ " ADD " + Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)");
+ db.execSQL(
+ "ALTER TABLE " + Tables.RAW_CONTACTS +
+ " ADD " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
+ + " INTEGER NOT NULL DEFAULT 0");
+
+ // For each Contact, find the RawContact that contributed the display name
+ db.execSQL(
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" +
+ " SELECT " + RawContacts._ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
+ " AND " + RawContactsColumns.CONCRETE_DISPLAY_NAME + "=" +
+ Tables.CONTACTS + "." + Contacts.DISPLAY_NAME +
+ " ORDER BY " + RawContacts._ID +
+ " LIMIT 1)"
+ );
+
+ db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" +
+ Contacts.NAME_RAW_CONTACT_ID +
+ ");");
+
+ // If for some unknown reason we missed some names, let's make sure there are
+ // no contacts without a name, picking a raw contact "at random".
+ db.execSQL(
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" +
+ " SELECT " + RawContacts._ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
+ " ORDER BY " + RawContacts._ID +
+ " LIMIT 1)" +
+ " WHERE " + Contacts.NAME_RAW_CONTACT_ID + " IS NULL"
+ );
+
+ // Wipe out DISPLAY_NAME on the Contacts table as it is no longer in use.
+ db.execSQL(
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.DISPLAY_NAME + "=NULL"
+ );
+
+ // Copy the IN_VISIBLE_GROUP flag down to all raw contacts to allow
+ // indexing on (display_name, in_visible_group)
+ db.execSQL(
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" +
+ "SELECT " + Contacts.IN_VISIBLE_GROUP +
+ " FROM " + Tables.CONTACTS +
+ " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")"
+ );
+
+ db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
+ RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+ RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" +
+ ");");
+
+ db.execSQL("DROP INDEX contacts_visible_index");
+ db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" +
+ Contacts.IN_VISIBLE_GROUP +
+ ");");
+ }
+
/**
* Adds index stats into the SQLite database to force it to always use the lookup indexes.
*/
@@ -1376,6 +1532,8 @@
updateIndexStats(db, Tables.PHONE_LOOKUP,
"phone_lookup_index", "10000 2 2 1");
+ updateIndexStats(db, Tables.PHONE_LOOKUP,
+ "phone_lookup_min_match_index", "10000 2 2 1");
updateIndexStats(db, Tables.DATA,
"data_mimetype_data1_index", "60000 5000 2");
@@ -1414,6 +1572,8 @@
close();
db = super.getWritableDatabase();
}
+ // let {@link SQLiteDatabase} cache my compiled-sql statements.
+ db.setMaxSqlCacheSize(MAX_CACHE_SIZE_FOR_CONTACTS_DB);
return db;
}
@@ -1435,8 +1595,6 @@
db.execSQL("DELETE FROM " + Tables.CALLS + ";");
// Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
-
- db.execSQL("VACUUM;");
}
/**
@@ -1554,16 +1712,20 @@
final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
mVisibleUpdate.bindLong(1, groupMembershipMimetypeId);
mVisibleUpdate.execute();
+ mVisibleUpdateRawContacts.execute();
}
/**
* Update {@link Contacts#IN_VISIBLE_GROUP} for a specific contact.
*/
- public void updateContactVisible(long aggId) {
+ public void updateContactVisible(long contactId) {
final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
mVisibleSpecificUpdate.bindLong(1, groupMembershipMimetypeId);
- mVisibleSpecificUpdate.bindLong(2, aggId);
+ mVisibleSpecificUpdate.bindLong(2, contactId);
mVisibleSpecificUpdate.execute();
+
+ mVisibleSpecificUpdateRawContacts.bindLong(1, contactId);
+ mVisibleSpecificUpdateRawContacts.execute();
}
/**
@@ -1592,25 +1754,25 @@
}
public void buildPhoneLookupAndRawContactQuery(SQLiteQueryBuilder qb, String number) {
- String normalizedNumber = PhoneNumberUtils.toCallerIDMinMatch(number);
+ String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
qb.setTables(Tables.DATA_JOIN_RAW_CONTACTS +
" JOIN " + Tables.PHONE_LOOKUP
+ " ON(" + DataColumns.CONCRETE_ID + "=" + PhoneLookupColumns.DATA_ID + ")");
StringBuilder sb = new StringBuilder();
- sb.append(PhoneLookupColumns.NORMALIZED_NUMBER + " GLOB '");
- sb.append(normalizedNumber);
- sb.append("*' AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
+ sb.append(PhoneLookupColumns.MIN_MATCH + "='");
+ sb.append(minMatch);
+ sb.append("' AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
DatabaseUtils.appendEscapedSQLString(sb, number);
- sb.append(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
+ sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
qb.appendWhere(sb.toString());
}
public void buildPhoneLookupAndContactQuery(SQLiteQueryBuilder qb, String number) {
- String normalizedNumber = PhoneNumberUtils.toCallerIDMinMatch(number);
+ String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
StringBuilder sb = new StringBuilder();
- appendPhoneLookupTables(sb, normalizedNumber, true);
+ appendPhoneLookupTables(sb, minMatch, true);
qb.setTables(sb.toString());
sb = new StringBuilder();
@@ -1620,33 +1782,37 @@
public String buildPhoneLookupAsNestedQuery(String number) {
StringBuilder sb = new StringBuilder();
- final String normalizedNumber = PhoneNumberUtils.toCallerIDMinMatch(number);
+ final String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
sb.append("(SELECT DISTINCT raw_contact_id" + " FROM ");
- appendPhoneLookupTables(sb, normalizedNumber, false);
+ appendPhoneLookupTables(sb, minMatch, false);
sb.append(" WHERE ");
appendPhoneLookupSelection(sb, number);
sb.append(")");
return sb.toString();
}
- private void appendPhoneLookupTables(StringBuilder sb, final String normalizedNumber,
+ private void appendPhoneLookupTables(StringBuilder sb, final String minMatch,
boolean joinContacts) {
sb.append(Tables.RAW_CONTACTS);
if (joinContacts) {
- sb.append(" JOIN " + getContactView() + " contacts"
- + " ON (contacts._id = raw_contacts.contact_id)");
+ sb.append(" JOIN " + getContactView() + " contacts_view"
+ + " ON (contacts_view._id = raw_contacts.contact_id)");
}
sb.append(", (SELECT data_id FROM phone_lookup "
- + "WHERE (phone_lookup.normalized_number GLOB '");
- sb.append(normalizedNumber);
- sb.append("*')) AS lookup, " + Tables.DATA);
+ + "WHERE (" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.MIN_MATCH + " = '");
+ sb.append(minMatch);
+ sb.append("')) AS lookup, " + Tables.DATA);
}
private void appendPhoneLookupSelection(StringBuilder sb, String number) {
sb.append("lookup.data_id=data._id AND data.raw_contact_id=raw_contacts._id"
+ " AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
DatabaseUtils.appendEscapedSQLString(sb, number);
- sb.append(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
+ sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
+ }
+
+ public String getUseStrictPhoneNumberComparisonParameter() {
+ return mUseStrictPhoneNumberComparison ? "1" : "0";
}
/**
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 57e1e5d..807a46b 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -60,6 +60,7 @@
import android.content.UriMatcher;
import android.content.SharedPreferences.Editor;
import android.content.res.AssetFileDescriptor;
+import android.database.CharArrayBuffer;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteConstraintException;
@@ -110,6 +111,7 @@
import java.io.OutputStream;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
+import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -214,20 +216,6 @@
private static final int RAW_CONTACT_ENTITIES = 15001;
- private interface ContactsQuery {
- public static final String TABLE = Tables.RAW_CONTACTS;
-
- public static final String[] PROJECTION = new String[] {
- RawContactsColumns.CONCRETE_ID,
- RawContacts.ACCOUNT_NAME,
- RawContacts.ACCOUNT_TYPE,
- };
-
- public static final int RAW_CONTACT_ID = 0;
- public static final int ACCOUNT_NAME = 1;
- public static final int ACCOUNT_TYPE = 2;
- }
-
private interface DataContactsQuery {
public static final String TABLE = "data "
+ "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
@@ -244,22 +232,6 @@
public static final int CONTACT_ID = 2;
}
- private interface DisplayNameQuery {
- public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
-
- public static final String[] COLUMNS = new String[] {
- MimetypesColumns.MIMETYPE,
- Data.IS_PRIMARY,
- Data.DATA1,
- Organization.TITLE,
- };
-
- public static final int MIMETYPE = 0;
- public static final int IS_PRIMARY = 1;
- public static final int DATA = 2;
- public static final int TITLE = 3;
- }
-
private interface DataDeleteQuery {
public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
@@ -295,39 +267,18 @@
}
- private interface NicknameLookupQuery {
- String TABLE = Tables.NICKNAME_LOOKUP;
-
- String[] COLUMNS = new String[] {
- NicknameLookupColumns.CLUSTER
- };
-
- int CLUSTER = 0;
- }
-
private interface RawContactsQuery {
String TABLE = Tables.RAW_CONTACTS;
String[] COLUMNS = new String[] {
- ContactsContract.RawContacts.DELETED
+ RawContacts.DELETED,
+ RawContacts.ACCOUNT_TYPE,
+ RawContacts.ACCOUNT_NAME,
};
int DELETED = 0;
- }
-
- private static final HashMap<String, Integer> sDisplayNameSources;
- static {
- sDisplayNameSources = new HashMap<String, Integer>();
- sDisplayNameSources.put(StructuredName.CONTENT_ITEM_TYPE,
- DisplayNameSources.STRUCTURED_NAME);
- sDisplayNameSources.put(Nickname.CONTENT_ITEM_TYPE,
- DisplayNameSources.NICKNAME);
- sDisplayNameSources.put(Organization.CONTENT_ITEM_TYPE,
- DisplayNameSources.ORGANIZATION);
- sDisplayNameSources.put(Phone.CONTENT_ITEM_TYPE,
- DisplayNameSources.PHONE);
- sDisplayNameSources.put(Email.CONTENT_ITEM_TYPE,
- DisplayNameSources.EMAIL);
+ int ACCOUNT_TYPE = 1;
+ int ACCOUNT_NAME = 2;
}
public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
@@ -347,6 +298,18 @@
+ " FROM " + Tables.GROUPS
+ " WHERE " + Groups.TITLE + "=?)))";
+ /** Sql for updating DIRTY flag on multiple raw contacts */
+ private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL =
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContacts.DIRTY + "=1" +
+ " WHERE " + RawContacts._ID + " IN (";
+
+ /** Sql for updating VERSION on multiple raw contacts */
+ private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" +
+ " WHERE " + RawContacts._ID + " IN (";
+
/** Contains just BaseColumns._COUNT */
private static final HashMap<String, String> sCountProjectionMap;
/** Contains just the contacts columns */
@@ -379,6 +342,12 @@
/** Contains Live Folders columns */
private static final HashMap<String, String> sLiveFoldersProjectionMap;
+ // where clause to update the status_updates table
+ private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
+ StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID +
+ " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE +
+ " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE ";
+
/** Precompiled sql statement for setting a data record to the primary. */
private SQLiteStatement mSetPrimaryStatement;
/** Precompiled sql statement for setting a data record to the super primary. */
@@ -387,8 +356,6 @@
private SQLiteStatement mContactsLastTimeContactedUpdate;
/** Precompiled sql statement for updating a contact display name */
private SQLiteStatement mRawContactDisplayNameUpdate;
- /** Precompiled sql statement for marking a raw contact as dirty */
- private SQLiteStatement mRawContactDirtyUpdate;
/** Precompiled sql statement for updating an aggregated status update */
private SQLiteStatement mLastStatusUpdate;
private SQLiteStatement mNameLookupInsert;
@@ -401,7 +368,15 @@
private long mMimeTypeIdEmail;
private long mMimeTypeIdIm;
+ private long mMimeTypeIdStructuredName;
+ private long mMimeTypeIdOrganization;
+ private long mMimeTypeIdNickname;
+ private long mMimeTypeIdPhone;
private StringBuilder mSb = new StringBuilder();
+ private String[] mSelectionArgs1 = new String[1];
+ private String[] mSelectionArgs2 = new String[2];
+ private String[] mSelectionArgs3 = new String[3];
+ private Account mAccount;
static {
// Contacts URI matching table
@@ -732,28 +707,37 @@
sPhoneLookupProjectionMap = new HashMap<String, String>();
sPhoneLookupProjectionMap.put(PhoneLookup._ID,
- ContactsColumns.CONCRETE_ID + " AS " + PhoneLookup._ID);
+ "contacts_view." + Contacts._ID
+ + " AS " + PhoneLookup._ID);
sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY,
- Contacts.LOOKUP_KEY + " AS " + PhoneLookup.LOOKUP_KEY);
+ "contacts_view." + Contacts.LOOKUP_KEY
+ + " AS " + PhoneLookup.LOOKUP_KEY);
sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME,
- ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + PhoneLookup.DISPLAY_NAME);
+ "contacts_view." + Contacts.DISPLAY_NAME
+ + " AS " + PhoneLookup.DISPLAY_NAME);
sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED,
- ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
+ "contacts_view." + Contacts.LAST_TIME_CONTACTED
+ " AS " + PhoneLookup.LAST_TIME_CONTACTED);
sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED,
- ContactsColumns.CONCRETE_TIMES_CONTACTED + " AS " + PhoneLookup.TIMES_CONTACTED);
+ "contacts_view." + Contacts.TIMES_CONTACTED
+ + " AS " + PhoneLookup.TIMES_CONTACTED);
sPhoneLookupProjectionMap.put(PhoneLookup.STARRED,
- ContactsColumns.CONCRETE_STARRED + " AS " + PhoneLookup.STARRED);
+ "contacts_view." + Contacts.STARRED
+ + " AS " + PhoneLookup.STARRED);
sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP,
- Contacts.IN_VISIBLE_GROUP + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
+ "contacts_view." + Contacts.IN_VISIBLE_GROUP
+ + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID,
- Contacts.PHOTO_ID + " AS " + PhoneLookup.PHOTO_ID);
+ "contacts_view." + Contacts.PHOTO_ID
+ + " AS " + PhoneLookup.PHOTO_ID);
sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE,
- ContactsColumns.CONCRETE_CUSTOM_RINGTONE + " AS " + PhoneLookup.CUSTOM_RINGTONE);
+ "contacts_view." + Contacts.CUSTOM_RINGTONE
+ + " AS " + PhoneLookup.CUSTOM_RINGTONE);
sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER,
- Contacts.HAS_PHONE_NUMBER + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
+ "contacts_view." + Contacts.HAS_PHONE_NUMBER
+ + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL,
- ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
+ "contacts_view." + Contacts.SEND_TO_VOICEMAIL
+ " AS " + PhoneLookup.SEND_TO_VOICEMAIL);
sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER,
Phone.NUMBER + " AS " + PhoneLookup.NUMBER);
@@ -875,6 +859,7 @@
protected final String mMimetype;
protected long mMimetypeId;
+ @SuppressWarnings("all")
public DataRowHandler(String mimetype) {
mMimetype = mimetype;
@@ -933,7 +918,8 @@
}
if (values.size() > 0) {
- mDb.update(Tables.DATA, values, Data._ID + " = " + dataId, null);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ mDb.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1);
}
if (!callerIsSyncAdapter) {
@@ -945,8 +931,10 @@
long dataId = c.getLong(DataDeleteQuery._ID);
long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
- int count = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
- db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1);
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
if (count != 0 && primary) {
fixPrimary(db, rawContactId);
}
@@ -954,16 +942,15 @@
}
private void fixPrimary(SQLiteDatabase db, long rawContactId) {
- long newPrimaryId = findNewPrimaryDataId(db, rawContactId);
- if (newPrimaryId != -1) {
- setIsPrimary(rawContactId, newPrimaryId, getMimeTypeId());
- }
- }
-
- protected long findNewPrimaryDataId(SQLiteDatabase db, long rawContactId) {
+ long mimeTypeId = getMimeTypeId();
long primaryId = -1;
int primaryType = -1;
- Cursor c = queryData(db, rawContactId);
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.query(DataDeleteQuery.TABLE,
+ DataDeleteQuery.CONCRETE_COLUMNS,
+ Data.RAW_CONTACT_ID + "=?" +
+ " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId,
+ mSelectionArgs1, null, null, null);
try {
while (c.moveToNext()) {
long dataId = c.getLong(DataDeleteQuery._ID);
@@ -976,7 +963,9 @@
} finally {
c.close();
}
- return primaryId;
+ if (primaryId != -1) {
+ setIsPrimary(rawContactId, primaryId, mimeTypeId);
+ }
}
/**
@@ -987,50 +976,10 @@
return 0;
}
- protected Cursor queryData(SQLiteDatabase db, long rawContactId) {
- return db.query(DataDeleteQuery.TABLE, DataDeleteQuery.CONCRETE_COLUMNS,
- Data.RAW_CONTACT_ID + "=" + rawContactId +
- " AND " + MimetypesColumns.MIMETYPE + "='" + mMimetype + "'",
- null, null, null, null);
- }
-
protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
- String bestDisplayName = null;
- int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
-
- Cursor c = db.query(DisplayNameQuery.TABLE, DisplayNameQuery.COLUMNS,
- Data.RAW_CONTACT_ID + "=" + rawContactId, null, null, null, null);
- try {
- while (c.moveToNext()) {
- String mimeType = c.getString(DisplayNameQuery.MIMETYPE);
-
- // Display name is at DATA1 in all type. This is ensured in the constructor.
- String name = c.getString(DisplayNameQuery.DATA);
- if (TextUtils.isEmpty(name)
- && Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
- name = c.getString(DisplayNameQuery.TITLE);
- }
- boolean primary = StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)
- || (c.getInt(DisplayNameQuery.IS_PRIMARY) != 0);
-
- if (name != null) {
- Integer source = sDisplayNameSources.get(mimeType);
- if (source != null
- && (source > bestDisplayNameSource
- || (source == bestDisplayNameSource && primary))) {
- bestDisplayNameSource = source;
- bestDisplayName = name;
- }
- }
- }
-
- } finally {
- c.close();
- }
-
- setDisplayName(rawContactId, bestDisplayName, bestDisplayNameSource);
if (!isNewRawContact(rawContactId)) {
- mContactAggregator.updateDisplayName(db, rawContactId);
+ updateRawContactDisplayName(db, rawContactId);
+ mContactAggregator.updateDisplayNameForRawContact(db, rawContactId);
}
}
@@ -1045,8 +994,9 @@
public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
ContentValues update) {
final ContentValues values = new ContentValues();
- final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=" + dataId,
- null, null, null, null);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?",
+ mSelectionArgs1, null, null, null);
try {
if (cursor.moveToFirst()) {
for (int i = 0; i < cursor.getColumnCount(); i++) {
@@ -1491,9 +1441,13 @@
phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
phoneValues.put(PhoneLookupColumns.DATA_ID, dataId);
phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
+ phoneValues.put(PhoneLookupColumns.MIN_MATCH,
+ PhoneNumberUtils.toCallerIDMinMatch(number));
+
db.replace(Tables.PHONE_LOOKUP, null, phoneValues);
} else {
- db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=" + dataId, null);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=?", mSelectionArgs1);
}
}
@@ -1573,7 +1527,8 @@
if (containsGroupSourceId) {
final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
- final long groupId = getOrMakeGroup(db, rawContactId, sourceId);
+ final long groupId = getOrMakeGroup(db, rawContactId, sourceId,
+ mInsertedRawContacts.get(rawContactId));
values.remove(GroupMembership.GROUP_SOURCE_ID);
values.put(GroupMembership.GROUP_ROW_ID, groupId);
}
@@ -1622,45 +1577,54 @@
}
}
+ /**
+ * An entry in group id cache. It maps the combination of (account type, account name
+ * and source id) to group row id.
+ */
+ public class GroupIdCacheEntry {
+ String accountType;
+ String accountName;
+ String sourceId;
+ long groupId;
+ }
private HashMap<String, DataRowHandler> mDataRowHandlers;
- private final ContactAggregationScheduler mAggregationScheduler;
private ContactsDatabaseHelper mDbHelper;
private NameSplitter mNameSplitter;
private NameLookupBuilder mNameLookupBuilder;
- private HashMap<String, SoftReference<String[]>> mNicknameClusterCache =
- new HashMap<String, SoftReference<String[]>>();
+
+ // We will use this much memory (in bits) to optimize the nickname cluster lookup
+ private static final int NICKNAME_BLOOM_FILTER_SIZE = 0x1FFF; // =long[128]
+ private BitSet mNicknameBloomFilter;
+
+ private HashMap<String, SoftReference<String[]>> mNicknameClusterCache = Maps.newHashMap();
+
private PostalSplitter mPostalSplitter;
+ // We don't need a soft cache for groups - the assumption is that there will only
+ // be a small number of contact groups. The cache is keyed off source id. The value
+ // is a list of groups with this group id.
+ private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
+
private ContactAggregator mContactAggregator;
private LegacyApiSupport mLegacyApiSupport;
private GlobalSearchSupport mGlobalSearchSupport;
private ContentValues mValues = new ContentValues();
+ private CharArrayBuffer mCharArrayBuffer = new CharArrayBuffer(128);
private volatile CountDownLatch mAccessLatch;
- private boolean mImportMode;
- private HashSet<Long> mInsertedRawContacts = Sets.newHashSet();
+ private HashMap<Long, Account> mInsertedRawContacts = Maps.newHashMap();
private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet();
+ private HashSet<Long> mDirtyRawContacts = Sets.newHashSet();
private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap();
private boolean mVisibleTouched = false;
private boolean mSyncToNetwork;
- public ContactsProvider2() {
- this(new ContactAggregationScheduler());
- }
-
- /**
- * Constructor for testing.
- */
- /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) {
- mAggregationScheduler = scheduler;
- }
-
@Override
public boolean onCreate() {
super.onCreate();
@@ -1677,7 +1641,7 @@
mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
mGlobalSearchSupport = new GlobalSearchSupport(this);
mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport);
- mContactAggregator = new ContactAggregator(this, mDbHelper, mAggregationScheduler);
+ mContactAggregator = new ContactAggregator(this, mDbHelper);
mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
final SQLiteDatabase db = mDbHelper.getReadableDatabase();
@@ -1711,12 +1675,9 @@
+ RawContactsColumns.DISPLAY_NAME_SOURCE + "=?" +
" WHERE " + RawContacts._ID + "=?");
- mRawContactDirtyUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
- + RawContacts.DIRTY + "=1 WHERE " + RawContacts._ID + "=?");
-
mLastStatusUpdate = db.compileStatement(
- "UPDATE " + Tables.CONTACTS
- + " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
"(SELECT " + DataColumns.CONCRETE_ID +
" FROM " + Tables.STATUS_UPDATES +
" JOIN " + Tables.DATA +
@@ -1728,8 +1689,8 @@
" WHERE " + RawContacts.CONTACT_ID + "=?" +
" ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
+ StatusUpdates.STATUS +
- " LIMIT 1)"
- + " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
+ " LIMIT 1)" +
+ " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
final Locale locale = Locale.getDefault();
mNameSplitter = new NameSplitter(
@@ -1810,6 +1771,11 @@
mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
+ mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
+ mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
+ mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE);
+ mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+ preloadNicknameBloomFilter();
return (db != null);
}
@@ -1824,10 +1790,6 @@
return ContactsDatabaseHelper.getInstance(context);
}
- /* package */ ContactAggregationScheduler getContactAggregationScheduler() {
- return mAggregationScheduler;
- }
-
/* package */ NameSplitter getNameSplitter() {
return mNameSplitter;
}
@@ -1852,6 +1814,7 @@
@Override
public void run() {
if (importLegacyContacts()) {
+ // TODO aggregate all newly added raw contacts
/*
* When the import process is done, we can unlock the provider and
@@ -1859,7 +1822,6 @@
*/
mAccessLatch.countDown();
mAccessLatch = null;
- scheduleContactAggregation();
}
}
};
@@ -1884,7 +1846,6 @@
/* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
boolean aggregatorEnabled = mContactAggregator.isEnabled();
mContactAggregator.setEnabled(false);
- mImportMode = true;
try {
importer.importContacts();
mContactAggregator.setEnabled(aggregatorEnabled);
@@ -1892,20 +1853,9 @@
} catch (Throwable e) {
Log.e(TAG, "Legacy contact import failed", e);
return false;
- } finally {
- mImportMode = false;
}
}
- @Override
- protected void finalize() throws Throwable {
- if (mContactAggregator != null) {
- mContactAggregator.quit();
- }
-
- super.finalize();
- }
-
/**
* Wipes all data from the contacts database.
*/
@@ -1974,10 +1924,12 @@
mInsertedRawContacts.clear();
mUpdatedRawContacts.clear();
mUpdatedSyncStates.clear();
+ mDirtyRawContacts.clear();
}
@Override
protected void beforeTransactionCommit() {
+
if (VERBOSE_LOGGING) {
Log.v(TAG, "beforeTransactionCommit");
}
@@ -1994,15 +1946,26 @@
if (VERBOSE_LOGGING) {
Log.v(TAG, "flushTransactionChanges");
}
- for (long rawContactId : mInsertedRawContacts) {
- mContactAggregator.insertContact(mDb, rawContactId);
+
+ for (long rawContactId : mInsertedRawContacts.keySet()) {
+ updateRawContactDisplayName(mDb, rawContactId);
+ mContactAggregator.onRawContactInsert(mDb, rawContactId);
}
- String ids;
+ if (!mDirtyRawContacts.isEmpty()) {
+ mSb.setLength(0);
+ mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
+ appendIds(mSb, mDirtyRawContacts);
+ mSb.append(")");
+ mDb.execSQL(mSb.toString());
+ }
+
if (!mUpdatedRawContacts.isEmpty()) {
- ids = buildIdsString(mUpdatedRawContacts);
- mDb.execSQL("UPDATE raw_contacts SET version = version + 1 WHERE _id in " + ids,
- new Object[]{});
+ mSb.setLength(0);
+ mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
+ appendIds(mSb, mUpdatedRawContacts);
+ mSb.append(")");
+ mDb.execSQL(mSb.toString());
}
for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) {
@@ -2013,19 +1976,16 @@
clearTransactionalChanges();
}
- private String buildIdsString(HashSet<Long> ids) {
- StringBuilder idsBuilder = null;
+ /**
+ * Appends comma separated ids.
+ * @param ids Should not be empty
+ */
+ private void appendIds(StringBuilder sb, HashSet<Long> ids) {
for (long id : ids) {
- if (idsBuilder == null) {
- idsBuilder = new StringBuilder();
- idsBuilder.append("(");
- } else {
- idsBuilder.append(",");
- }
- idsBuilder.append(id);
+ sb.append(id).append(',');
}
- idsBuilder.append(")");
- return idsBuilder.toString();
+
+ sb.setLength(sb.length() - 1); // Yank the last comma
}
@Override
@@ -2039,12 +1999,8 @@
syncToNetwork);
}
- protected void scheduleContactAggregation() {
- mContactAggregator.schedule();
- }
-
private boolean isNewRawContact(long rawContactId) {
- return mInsertedRawContacts.contains(rawContactId);
+ return mInsertedRawContacts.containsKey(rawContactId);
}
private DataRowHandler getDataRowHandler(final String mimeType) {
@@ -2059,7 +2015,7 @@
@Override
protected Uri insertInTransaction(Uri uri, ContentValues values) {
if (VERBOSE_LOGGING) {
- Log.v(TAG, "insertInTransaction: " + uri);
+ Log.v(TAG, "insertInTransaction: " + uri + " " + values);
}
final boolean callerIsSyncAdapter =
@@ -2079,8 +2035,7 @@
}
case RAW_CONTACTS: {
- final Account account = readAccountFromQueryParams(uri);
- id = insertRawContact(values, account);
+ id = insertRawContact(uri, values);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
@@ -2099,8 +2054,7 @@
}
case GROUPS: {
- final Account account = readAccountFromQueryParams(uri);
- id = insertGroup(uri, values, account, callerIsSyncAdapter);
+ id = insertGroup(uri, values, callerIsSyncAdapter);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
@@ -2131,25 +2085,49 @@
/**
* If account is non-null then store it in the values. If the account is already
* specified in the values then it must be consistent with the account, if it is non-null.
- * @param values the ContentValues to read from and update
- * @param account the explicitly provided Account
- * @return false if the accounts are inconsistent
+ * @param uri the ContentValues to read from and update
+ * @param values the explicitly provided Account
+ * @return false if the parameters are inconsistent
*/
- private boolean resolveAccount(ContentValues values, Account account) {
- // If either is specified then both must be specified.
- final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
- final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
- if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
- final Account valuesAccount = new Account(accountName, accountType);
- if (account != null && !valuesAccount.equals(account)) {
+ private boolean resolveAccount(Uri uri, ContentValues values) {
+ String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+ String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
+
+ if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
+ accountName = null;
+ accountType = null;
+ }
+
+ String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
+ String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+
+ if (TextUtils.isEmpty(valueAccountName) && TextUtils.isEmpty(valueAccountType)) {
+ values.put(RawContacts.ACCOUNT_NAME, accountName);
+ values.put(RawContacts.ACCOUNT_TYPE, accountType);
+ } else {
+ if (accountName != null && !accountName.equals(valueAccountName)) {
return false;
}
- account = valuesAccount;
+
+ if (accountType != null && !accountType.equals(valueAccountType)) {
+ return false;
+ }
+
+ accountName = valueAccountName;
+ accountType = valueAccountType;
}
- if (account != null) {
- values.put(RawContacts.ACCOUNT_NAME, account.name);
- values.put(RawContacts.ACCOUNT_TYPE, account.type);
+
+ if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
+ mAccount = null;
+ return true;
}
+
+ if (mAccount == null
+ || !mAccount.name.equals(accountName)
+ || !mAccount.type.equals(accountType)) {
+ mAccount = new Account(accountName, accountType);
+ }
+
return true;
}
@@ -2166,29 +2144,30 @@
/**
* Inserts an item in the contacts table
*
- * @param values the values for the new row
- * @param account the account this contact should be associated with. may be null.
+ * @param uri the values for the new row
+ * @param values the account this contact should be associated with. may be null.
* @return the row ID of the newly created row
*/
- private long insertRawContact(ContentValues values, Account account) {
- ContentValues overriddenValues = new ContentValues(values);
- overriddenValues.putNull(RawContacts.CONTACT_ID);
- if (!resolveAccount(overriddenValues, account)) {
+ private long insertRawContact(Uri uri, ContentValues values) {
+ mValues.clear();
+ mValues.putAll(values);
+ mValues.putNull(RawContacts.CONTACT_ID);
+
+ if (!resolveAccount(uri, mValues)) {
return -1;
}
if (values.containsKey(RawContacts.DELETED)
&& values.getAsInteger(RawContacts.DELETED) != 0) {
- overriddenValues.put(RawContacts.AGGREGATION_MODE,
- RawContacts.AGGREGATION_MODE_DISABLED);
+ mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
}
- long rawContactId =
- mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
+ long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues);
mContactAggregator.markNewForAggregation(rawContactId);
// Trigger creation of a Contact based on this RawContact at the end of transaction
- mInsertedRawContacts.add(rawContactId);
+ mInsertedRawContacts.put(rawContactId, mAccount);
+
return rawContactId;
}
@@ -2276,35 +2255,60 @@
* @throws IllegalArgumentException if the contact is not associated with an account
* @throws IllegalStateException if a group needs to be created but the creation failed
*/
- private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId) {
- Account account = null;
- Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts._ID + "="
- + rawContactId, null, null, null, null);
- try {
- if (c.moveToNext()) {
- final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME);
- final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE);
- if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
- account = new Account(accountName, accountType);
+ private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId,
+ Account account) {
+
+ if (account == null) {
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
+ RawContacts._ID + "=?", mSelectionArgs1, null, null, null);
+ try {
+ if (c.moveToFirst()) {
+ String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME);
+ String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
+ if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+ account = new Account(accountName, accountType);
+ }
}
+ } finally {
+ c.close();
}
- } finally {
- c.close();
}
+
if (account == null) {
throw new IllegalArgumentException("if the groupmembership only "
- + "has a sourceid the the contact must be associate with "
+ + "has a sourceid the the contact must be associated with "
+ "an account");
}
+ ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId);
+ if (entries == null) {
+ entries = new ArrayList<GroupIdCacheEntry>(1);
+ mGroupIdCache.put(sourceId, entries);
+ }
+
+ int count = entries.size();
+ for (int i = 0; i < count; i++) {
+ GroupIdCacheEntry entry = entries.get(i);
+ if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) {
+ return entry.groupId;
+ }
+ }
+
+ GroupIdCacheEntry entry = new GroupIdCacheEntry();
+ entry.accountName = account.name;
+ entry.accountType = account.type;
+ entry.sourceId = sourceId;
+ entries.add(0, entry);
+
// look up the group that contains this sourceId and has the same account name and type
// as the contact refered to by rawContactId
- c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
+ Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
new String[]{sourceId, account.name, account.type}, null, null, null);
try {
- if (c.moveToNext()) {
- return c.getLong(0);
+ if (c.moveToFirst()) {
+ entry.groupId = c.getLong(0);
} else {
ContentValues groupValues = new ContentValues();
groupValues.put(Groups.ACCOUNT_NAME, account.name);
@@ -2315,11 +2319,94 @@
throw new IllegalStateException("unable to create a new group with "
+ "this sourceid: " + groupValues);
}
- return groupId;
+ entry.groupId = groupId;
}
} finally {
c.close();
}
+
+ return entry.groupId;
+ }
+
+ private interface DisplayNameQuery {
+ public static final String RAW_SQL =
+ "SELECT "
+ + DataColumns.MIMETYPE_ID + ","
+ + Data.IS_PRIMARY + ","
+ + Data.DATA1 + ","
+ + Organization.TITLE +
+ " FROM " + Tables.DATA +
+ " WHERE " + Data.RAW_CONTACT_ID + "=?" +
+ " AND (" + Data.DATA1 + " NOT NULL OR " +
+ Organization.TITLE + " NOT NULL)";
+
+ public static final int MIMETYPE = 0;
+ public static final int IS_PRIMARY = 1;
+ public static final int DATA = 2;
+ public static final int TITLE = 3;
+ }
+
+ /**
+ * Updates a raw contact display name based on data rows, e.g. structured name,
+ * organization, email etc.
+ */
+ private void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
+ String bestDisplayName = null;
+ int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
+
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1);
+ try {
+ while (c.moveToNext()) {
+ int mimeType = c.getInt(DisplayNameQuery.MIMETYPE);
+
+ // Display name is at DATA1 in all type. This is ensured in the
+ // constructor.
+ mCharArrayBuffer.sizeCopied = 0;
+ c.copyStringToBuffer(DisplayNameQuery.DATA, mCharArrayBuffer);
+ if (mimeType == mMimeTypeIdOrganization && mCharArrayBuffer.sizeCopied == 0) {
+ c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer);
+ }
+
+ if (mCharArrayBuffer.sizeCopied != 0) {
+ int source = getDisplayNameSource(mimeType);
+ if (source > bestDisplayNameSource) {
+ bestDisplayNameSource = source;
+ bestDisplayName = new String(mCharArrayBuffer.data, 0,
+ mCharArrayBuffer.sizeCopied);
+ } else if (source == bestDisplayNameSource
+ && source != DisplayNameSources.UNDEFINED) {
+ if (mimeType == mMimeTypeIdStructuredName
+ || c.getInt(DisplayNameQuery.IS_PRIMARY) != 0) {
+ bestDisplayNameSource = source;
+ bestDisplayName = new String(mCharArrayBuffer.data, 0,
+ mCharArrayBuffer.sizeCopied);
+ }
+ }
+ }
+ }
+
+ } finally {
+ c.close();
+ }
+
+ setDisplayName(rawContactId, bestDisplayName, bestDisplayNameSource);
+ }
+
+ private int getDisplayNameSource(int mimeTypeId) {
+ if (mimeTypeId == mMimeTypeIdStructuredName) {
+ return DisplayNameSources.STRUCTURED_NAME;
+ } else if (mimeTypeId == mMimeTypeIdEmail) {
+ return DisplayNameSources.EMAIL;
+ } else if (mimeTypeId == mMimeTypeIdPhone) {
+ return DisplayNameSources.PHONE;
+ } else if (mimeTypeId == mMimeTypeIdOrganization) {
+ return DisplayNameSources.ORGANIZATION;
+ } else if (mimeTypeId == mMimeTypeIdNickname) {
+ return DisplayNameSources.NICKNAME;
+ } else {
+ return DisplayNameSources.UNDEFINED;
+ }
}
/**
@@ -2358,8 +2445,9 @@
// Note that the query will return data according to the access restrictions,
// so we don't need to worry about deleting data we don't have permission to read.
- Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=" + dataId, null,
- null);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?",
+ mSelectionArgs1, null);
try {
if (!c.moveToFirst()) {
@@ -2395,27 +2483,28 @@
/**
* Inserts an item in the groups table
*/
- private long insertGroup(Uri uri, ContentValues values, Account account,
- boolean callerIsSyncAdapter) {
- ContentValues overriddenValues = new ContentValues(values);
- if (!resolveAccount(overriddenValues, account)) {
+ private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
+ mValues.clear();
+ mValues.putAll(values);
+
+ if (!resolveAccount(uri, mValues)) {
return -1;
}
// Replace package with internal mapping
- final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE);
+ final String packageName = mValues.getAsString(Groups.RES_PACKAGE);
if (packageName != null) {
- overriddenValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
+ mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
}
- overriddenValues.remove(Groups.RES_PACKAGE);
+ mValues.remove(Groups.RES_PACKAGE);
if (!callerIsSyncAdapter) {
- overriddenValues.put(Groups.DIRTY, 1);
+ mValues.put(Groups.DIRTY, 1);
}
- long result = mDb.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
+ long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues);
- if (overriddenValues.containsKey(Groups.GROUP_VISIBLE)) {
+ if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
mVisibleTouched = true;
}
@@ -2661,12 +2750,15 @@
case RAW_CONTACTS: {
int numDeletes = 0;
- Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
+ Cursor c = mDb.query(Tables.RAW_CONTACTS,
+ new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
try {
while (c.moveToNext()) {
final long rawContactId = c.getLong(0);
- numDeletes += deleteRawContact(rawContactId, callerIsSyncAdapter);
+ long contactId = c.getLong(1);
+ numDeletes += deleteRawContact(rawContactId, contactId,
+ callerIsSyncAdapter);
}
} finally {
c.close();
@@ -2676,7 +2768,8 @@
case RAW_CONTACTS_ID: {
final long rawContactId = ContentUris.parseId(uri);
- return deleteRawContact(rawContactId, callerIsSyncAdapter);
+ return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId),
+ callerIsSyncAdapter);
}
case DATA: {
@@ -2691,7 +2784,8 @@
case POSTALS_ID: {
long dataId = ContentUris.parseId(uri);
mSyncToNetwork |= !callerIsSyncAdapter;
- return deleteData(Data._ID + "=" + dataId, null, callerIsSyncAdapter);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter);
}
case GROUPS_ID: {
@@ -2732,14 +2826,8 @@
}
}
- private static boolean readBooleanQueryParameter(Uri uri, String name, boolean defaultValue) {
- final String flag = uri.getQueryParameter(name);
- return flag == null
- ? defaultValue
- : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase()));
- }
-
public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
+ mGroupIdCache.clear();
final long groupMembershipMimetypeId = mDbHelper
.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
@@ -2781,10 +2869,13 @@
return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
}
- public int deleteRawContact(long rawContactId, boolean callerIsSyncAdapter) {
+ public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
+ mContactAggregator.invalidateAggregationExceptionCache();
if (callerIsSyncAdapter) {
mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
- return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
+ int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
+ mContactAggregator.updateDisplayNameForContact(mDb, contactId);
+ return count;
} else {
mDbHelper.removeContactIfSingleton(rawContactId);
return markRawContactAsDeleted(rawContactId);
@@ -2792,8 +2883,14 @@
}
private int deleteStatusUpdates(String selection, String[] selectionArgs) {
- // TODO delete from both tables: presence and status_updates
- return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
+ // delete from both tables: presence and status_updates
+ // TODO should account type/name be appended to the where clause?
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "deleting data from status_updates for " + selection);
+ }
+ mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection),
+ selectionArgs);
+ return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
}
private int markRawContactAsDeleted(long rawContactId) {
@@ -2820,7 +2917,7 @@
final int match = sUriMatcher.match(uri);
if (match == SYNCSTATE_ID && selection == null) {
long rowId = ContentUris.parseId(uri);
- Object data = values.get(ContactsContract.SyncStateColumns.DATA);
+ Object data = values.get(ContactsContract.SyncState.DATA);
mUpdatedSyncStates.put(rowId, data);
return 1;
}
@@ -2903,10 +3000,12 @@
case RAW_CONTACTS_ID: {
long rawContactId = ContentUris.parseId(uri);
if (selection != null) {
- count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+ count = updateRawContacts(values, RawContacts._ID + "=?"
+ " AND(" + selection + ")", selectionArgs);
} else {
- count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId, null);
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1);
}
break;
}
@@ -2922,7 +3021,8 @@
case GROUPS_ID: {
long groupId = ContentUris.parseId(uri);
- String selectionWithId = (Groups._ID + "=" + groupId + " ")
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId));
+ String selectionWithId = Groups._ID + "=? "
+ (selection == null ? "" : " AND " + selection);
count = updateGroups(uri, values, selectionWithId, selectionArgs,
callerIsSyncAdapter);
@@ -2943,6 +3043,11 @@
break;
}
+ case STATUS_UPDATES: {
+ count = updateStatusUpdate(uri, values, selection, selectionArgs);
+ break;
+ }
+
default: {
mSyncToNetwork = true;
return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
@@ -2952,9 +3057,68 @@
return count;
}
+ private int updateStatusUpdate(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ // update status_updates table, if status is provided
+ // TODO should account type/name be appended to the where clause?
+ int updateCount = 0;
+ ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values);
+ if (settableValues.size() > 0) {
+ updateCount = mDb.update(Tables.STATUS_UPDATES,
+ settableValues,
+ getWhereClauseForStatusUpdatesTable(selection),
+ selectionArgs);
+ }
+
+ // now update the Presence table
+ settableValues = getSettableColumnsForPresenceTable(values);
+ if (settableValues.size() > 0) {
+ updateCount = mDb.update(Tables.PRESENCE, settableValues,
+ selection, selectionArgs);
+ }
+ // TODO updateCount is not entirely a valid count of updated rows because 2 tables could
+ // potentially get updated in this method.
+ return updateCount;
+ }
+
+ /**
+ * Build a where clause to select the rows to be updated in status_updates table.
+ */
+ private String getWhereClauseForStatusUpdatesTable(String selection) {
+ mSb.setLength(0);
+ mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE);
+ mSb.append(selection);
+ mSb.append(")");
+ return mSb.toString();
+ }
+
+ private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) {
+ mValues.clear();
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values,
+ StatusUpdates.STATUS);
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values,
+ StatusUpdates.STATUS_TIMESTAMP);
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values,
+ StatusUpdates.STATUS_RES_PACKAGE);
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values,
+ StatusUpdates.STATUS_LABEL);
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values,
+ StatusUpdates.STATUS_ICON);
+ return mValues;
+ }
+
+ private ContentValues getSettableColumnsForPresenceTable(ContentValues values) {
+ mValues.clear();
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values,
+ StatusUpdates.PRESENCE);
+ return mValues;
+ }
+
private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
String[] selectionArgs, boolean callerIsSyncAdapter) {
+ mGroupIdCache.clear();
+
ContentValues updatedValues;
if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
updatedValues = mValues;
@@ -2972,8 +3136,9 @@
if (updatedValues.containsKey(Groups.SHOULD_SYNC)
&& updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
final long groupId = ContentUris.parseId(uri);
+ mSelectionArgs1[0] = String.valueOf(groupId);
Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
- Groups.ACCOUNT_TYPE}, Groups._ID + "=" + groupId, null, null,
+ Groups.ACCOUNT_TYPE}, Groups._ID + "=?", mSelectionArgs1, null,
null, null);
String accountName;
String accountType;
@@ -2983,7 +3148,7 @@
accountType = c.getString(1);
if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
Account account = new Account(accountName, accountType);
- ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
+ ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
new Bundle());
break;
}
@@ -3032,12 +3197,16 @@
final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
&& values.getAsInteger(RawContacts.DELETED) == 0);
int previousDeleted = 0;
+ String accountType = null;
+ String accountName = null;
if (requestUndoDelete) {
Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection,
null, null, null, null);
try {
if (cursor.moveToFirst()) {
previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
+ accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
+ accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
}
} finally {
cursor.close();
@@ -3055,7 +3224,7 @@
}
if (requestUndoDelete && previousDeleted == 1) {
// undo delete, needs aggregation again.
- mInsertedRawContacts.add(rawContactId);
+ mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType));
}
}
return count;
@@ -3164,7 +3333,8 @@
mValues.put(RawContacts.DIRTY, 1);
}
- mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=" + contactId, null);
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", mSelectionArgs1);
// Copy changeable values to prevent automatically managed fields from
// being explicitly updated by clients.
@@ -3180,7 +3350,7 @@
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
values, Contacts.STARRED);
- return mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=" + contactId, null);
+ return mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1);
}
public void updateContactLastContactedTime(long contactId, long lastTimeContacted) {
@@ -3204,9 +3374,11 @@
}
if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
+ mSelectionArgs2[0] = String.valueOf(rawContactId1);
+ mSelectionArgs2[1] = String.valueOf(rawContactId2);
db.delete(Tables.AGGREGATION_EXCEPTIONS,
- AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1 + " AND "
- + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2, null);
+ AggregationExceptions.RAW_CONTACT_ID1 + "=? AND "
+ + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2);
} else {
ContentValues exceptionValues = new ContentValues(3);
exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
@@ -3216,6 +3388,7 @@
exceptionValues);
}
+ mContactAggregator.invalidateAggregationExceptionCache();
mContactAggregator.markForAggregation(rawContactId1);
mContactAggregator.markForAggregation(rawContactId2);
@@ -3387,7 +3560,8 @@
case CONTACTS_ID: {
long contactId = ContentUris.parseId(uri);
setTablesAndProjectionMapForContacts(qb, uri, projection);
- qb.appendWhere(Contacts._ID + "=" + contactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+ qb.appendWhere(Contacts._ID + "=?");
break;
}
@@ -3403,10 +3577,17 @@
long contactId = Long.parseLong(pathSegments.get(3));
SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
- lookupQb.appendWhere(Contacts._ID + "=" + contactId + " AND " +
- Contacts.LOOKUP_KEY + "=");
- lookupQb.appendWhereEscapeString(lookupKey);
- Cursor c = query(db, lookupQb, projection, selection, selectionArgs, sortOrder,
+ String[] args;
+ if (selectionArgs == null) {
+ args = new String[2];
+ } else {
+ args = new String[selectionArgs.length + 2];
+ System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
+ }
+ args[0] = String.valueOf(contactId);
+ args[1] = lookupKey;
+ lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
+ Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
groupBy, limit);
if (c.getCount() != 0) {
return c;
@@ -3416,7 +3597,9 @@
}
setTablesAndProjectionMapForContacts(qb, uri, projection);
- qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
+ selectionArgs = insertSelectionArg(selectionArgs,
+ String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
+ qb.appendWhere(Contacts._ID + "=?");
break;
}
@@ -3425,7 +3608,9 @@
final String lookupKey = uri.getPathSegments().get(2);
qb.setTables(mDbHelper.getContactView(true /* require restricted */));
qb.setProjectionMap(sContactsVCardProjectionMap);
- qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
+ selectionArgs = insertSelectionArg(selectionArgs,
+ String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
+ qb.appendWhere(Contacts._ID + "=?");
break;
}
@@ -3505,14 +3690,16 @@
case CONTACTS_DATA: {
long contactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForData(qb, uri, projection, false);
- qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+ qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
break;
}
case CONTACTS_PHOTO: {
long contactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForData(qb, uri, projection, false);
- qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+ qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
break;
}
@@ -3525,8 +3712,9 @@
case PHONES_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
- qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
+ qb.appendWhere(" AND " + Data._ID + "=?");
break;
}
@@ -3536,7 +3724,7 @@
if (uri.getPathSegments().size() > 2) {
String filterParam = uri.getLastPathSegment();
StringBuilder sb = new StringBuilder();
- sb.append("(");
+ sb.append(" AND (");
boolean orNeeded = false;
String normalizedName = NameNormalizer.normalize(filterParam);
@@ -3560,7 +3748,7 @@
sb.append("')");
}
sb.append(")");
- qb.appendWhere(" AND " + sb);
+ qb.appendWhere(sb);
}
groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
if (sortOrder == null) {
@@ -3577,8 +3765,9 @@
case EMAILS_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
- qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
- qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"
+ + " AND " + Data._ID + "=?");
break;
}
@@ -3586,8 +3775,8 @@
setTablesAndProjectionMapForData(qb, uri, projection, false);
qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
if (uri.getPathSegments().size() > 2) {
- qb.appendWhere(" AND " + Email.DATA + "=");
- qb.appendWhereEscapeString(uri.getLastPathSegment());
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere(" AND " + Email.DATA + "=?");
}
break;
}
@@ -3653,9 +3842,10 @@
case POSTALS_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
+ StructuredPostal.CONTENT_ITEM_TYPE + "'");
- qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
+ qb.appendWhere(" AND " + Data._ID + "=?");
break;
}
@@ -3667,14 +3857,16 @@
case RAW_CONTACTS_ID: {
long rawContactId = ContentUris.parseId(uri);
setTablesAndProjectionMapForRawContacts(qb, uri);
- qb.appendWhere(" AND " + RawContacts._ID + "=" + rawContactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+ qb.appendWhere(" AND " + RawContacts._ID + "=?");
break;
}
case RAW_CONTACTS_DATA: {
long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForData(qb, uri, projection, false);
- qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=" + rawContactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+ qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?");
break;
}
@@ -3685,7 +3877,8 @@
case DATA_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
- qb.appendWhere(" AND " + Data._ID + "=" + ContentUris.parseId(uri));
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere(" AND " + Data._ID + "=?");
break;
}
@@ -3715,10 +3908,10 @@
}
case GROUPS_ID: {
- long groupId = ContentUris.parseId(uri);
qb.setTables(mDbHelper.getGroupView());
qb.setProjectionMap(sGroupsProjectionMap);
- qb.appendWhere(Groups._ID + "=" + groupId);
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere(Groups._ID + "=?");
break;
}
@@ -3783,7 +3976,8 @@
case STATUS_UPDATES_ID: {
setTableAndProjectionMapForStatusUpdates(qb, projection);
- qb.appendWhere(DataColumns.CONCRETE_ID + "=" + ContentUris.parseId(uri));
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere(DataColumns.CONCRETE_ID + "=?");
break;
}
@@ -3828,7 +4022,8 @@
case RAW_CONTACT_ENTITY_ID: {
long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForRawContactsEntities(qb, uri);
- qb.appendWhere(" AND " + RawContacts._ID + "=" + rawContactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+ qb.appendWhere(" AND " + RawContacts._ID + "=?");
break;
}
@@ -4043,7 +4238,7 @@
String[] projection) {
StringBuilder sb = new StringBuilder();
boolean excludeRestrictedData = false;
- String requestingPackage = uri.getQueryParameter(
+ String requestingPackage = getQueryParameter(uri,
ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
if (requestingPackage != null) {
excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
@@ -4072,7 +4267,7 @@
private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
StringBuilder sb = new StringBuilder();
boolean excludeRestrictedData = false;
- String requestingPackage = uri.getQueryParameter(
+ String requestingPackage = getQueryParameter(uri,
ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
if (requestingPackage != null) {
excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
@@ -4088,7 +4283,7 @@
boolean excludeRestrictedData = readBooleanQueryParameter(uri,
Data.FOR_EXPORT_ONLY, false);
- String requestingPackage = uri.getQueryParameter(
+ String requestingPackage = getQueryParameter(uri,
ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
if (requestingPackage != null) {
excludeRestrictedData = excludeRestrictedData
@@ -4106,7 +4301,7 @@
boolean excludeRestrictedData = readBooleanQueryParameter(uri,
Data.FOR_EXPORT_ONLY, false);
- String requestingPackage = uri.getQueryParameter(
+ String requestingPackage = getQueryParameter(uri,
ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
if (requestingPackage != null) {
excludeRestrictedData = excludeRestrictedData
@@ -4187,8 +4382,8 @@
}
private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
- final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
- final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
+ final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+ final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
if (!TextUtils.isEmpty(accountName)) {
qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
+ DatabaseUtils.sqlEscapeString(accountName) + " AND "
@@ -4200,8 +4395,8 @@
}
private String appendAccountToSelection(Uri uri, String selection) {
- final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
- final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
+ final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+ final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
if (!TextUtils.isEmpty(accountName)) {
StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
+ DatabaseUtils.sqlEscapeString(accountName) + " AND "
@@ -4224,8 +4419,8 @@
* @return A string containing a non-negative integer, or <code>null</code> if
* the parameter is not set, or is set to an invalid value.
*/
- private String getLimit(Uri url) {
- String limitParam = url.getQueryParameter("limit");
+ private String getLimit(Uri uri) {
+ String limitParam = getQueryParameter(uri, "limit");
if (limitParam == null) {
return null;
}
@@ -4277,7 +4472,7 @@
if (mDbHelper.hasAccessToRestrictedData()) {
return "1";
} else {
- return RawContacts.IS_RESTRICTED + "=0";
+ return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0";
}
}
@@ -4299,14 +4494,13 @@
throw new FileNotFoundException("Mode " + mode + " not supported.");
}
- long contactId = Long.parseLong(uri.getPathSegments().get(1));
-
String sql =
"SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() +
" WHERE " + Data._ID + "=" + Contacts.PHOTO_ID
- + " AND " + RawContacts.CONTACT_ID + "=" + contactId;
+ + " AND " + RawContacts.CONTACT_ID + "=?";
SQLiteDatabase db = mDbHelper.getReadableDatabase();
- return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, null);
+ return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql,
+ new String[]{uri.getPathSegments().get(1)});
}
case CONTACTS_AS_VCARD: {
@@ -4376,17 +4570,6 @@
composer.terminate();
}
-
- private static Account readAccountFromQueryParams(Uri uri) {
- final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
- final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
- if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
- return null;
- }
- return new Account(name, type);
- }
-
-
/**
* An implementation of EntityIterator that joins the contacts and data tables
* and consumes all the data rows for a contact in order to build the Entity for a contact.
@@ -4658,8 +4841,8 @@
if (groupIdString != null) {
qb.appendWhere(Groups._ID + "=" + groupIdString);
}
- final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME);
- final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE);
+ final String accountName = getQueryParameter(uri, Groups.ACCOUNT_NAME);
+ final String accountType = getQueryParameter(uri, Groups.ACCOUNT_TYPE);
if (!TextUtils.isEmpty(accountName)) {
qb.appendWhere(Groups.ACCOUNT_NAME + "="
+ DatabaseUtils.sqlEscapeString(accountName) + " AND "
@@ -4826,8 +5009,7 @@
* Sets the {@link RawContacts#DIRTY} for the specified raw contact.
*/
private void setRawContactDirty(long rawContactId) {
- mRawContactDirtyUpdate.bindLong(1, rawContactId);
- mRawContactDirtyUpdate.execute();
+ mDirtyRawContacts.add(rawContactId);
}
/*
@@ -4904,10 +5086,57 @@
mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name);
}
+ private interface NicknameLookupPreloadQuery {
+ String TABLE = Tables.NICKNAME_LOOKUP;
+
+ String[] COLUMNS = new String[] {
+ NicknameLookupColumns.NAME
+ };
+
+ int NAME = 0;
+ }
+
+ /**
+ * Read all known common nicknames from the database and populate a Bloom
+ * filter using the corresponding hash codes. The idea is to eliminate most
+ * of unnecessary database lookups for nicknames. Given a name, we will take
+ * its hash code and see if it is set in the Bloom filter. If not, we will know
+ * that the name is not in the database. If it is, we still need to run a
+ * query.
+ * <p>
+ * Given the size of the filter and the expected size of the nickname table,
+ * we should expect the combination of the Bloom filter and cache will
+ * prevent around 98-99% of unnecessary queries from running.
+ */
+ private void preloadNicknameBloomFilter() {
+ mNicknameBloomFilter = new BitSet(NICKNAME_BLOOM_FILTER_SIZE + 1);
+ SQLiteDatabase db = mDbHelper.getReadableDatabase();
+ Cursor cursor = db.query(NicknameLookupPreloadQuery.TABLE,
+ NicknameLookupPreloadQuery.COLUMNS,
+ null, null, null, null, null);
+ try {
+ int count = cursor.getCount();
+ for (int i = 0; i < count; i++) {
+ cursor.moveToNext();
+ String normalizedName = cursor.getString(NicknameLookupPreloadQuery.NAME);
+ int hashCode = normalizedName.hashCode();
+ mNicknameBloomFilter.set(hashCode & NICKNAME_BLOOM_FILTER_SIZE);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+
/**
* Returns nickname cluster IDs or null. Maintains cache.
*/
protected String[] getCommonNicknameClusters(String normalizedName) {
+ int hashCode = normalizedName.hashCode();
+ if (!mNicknameBloomFilter.get(hashCode & NICKNAME_BLOOM_FILTER_SIZE)) {
+ return null;
+ }
+
SoftReference<String[]> ref;
String[] clusters = null;
synchronized (mNicknameClusterCache) {
@@ -4930,6 +5159,16 @@
return clusters;
}
+ private interface NicknameLookupQuery {
+ String TABLE = Tables.NICKNAME_LOOKUP;
+
+ String[] COLUMNS = new String[] {
+ NicknameLookupColumns.CLUSTER
+ };
+
+ int CLUSTER = 0;
+ }
+
protected String[] loadNicknameClusters(String normalizedName) {
SQLiteDatabase db = mDbHelper.getReadableDatabase();
String[] clusters = null;
@@ -5019,7 +5258,7 @@
private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
String limit, boolean allowEmailMatch) {
sb.append("(" +
- "SELECT DISTINCT " + NameLookupColumns.RAW_CONTACT_ID +
+ "SELECT " + NameLookupColumns.RAW_CONTACT_ID +
" FROM " + Tables.NAME_LOOKUP +
" WHERE " + NameLookupColumns.NORMALIZED_NAME +
" GLOB '");
@@ -5091,7 +5330,75 @@
} catch (RemoteException e) {
Log.e(TAG, "Could not acquire sync adapter types");
}
-
return false;
}
+
+ /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
+ boolean defaultValue) {
+
+ // Manually parse the query, which is much faster than calling uri.getQueryParameter
+ String query = uri.getEncodedQuery();
+ if (query == null) {
+ return defaultValue;
+ }
+
+ int index = query.indexOf(parameter);
+ if (index == -1) {
+ return defaultValue;
+ }
+
+ index += parameter.length();
+
+ return !matchQueryParameter(query, index, "=0", false)
+ && !matchQueryParameter(query, index, "=false", true);
+ }
+
+ private static boolean matchQueryParameter(String query, int index, String value,
+ boolean ignoreCase) {
+ int length = value.length();
+ return query.regionMatches(ignoreCase, index, value, 0, length)
+ && (query.length() == index + length || query.charAt(index + length) == '&');
+ }
+
+ /**
+ * A fast re-implementation of {@link Uri#getQueryParameter}
+ */
+ /* package */ static String getQueryParameter(Uri uri, String parameter) {
+ String query = uri.getEncodedQuery();
+ if (query == null) {
+ return null;
+ }
+
+ int queryLength = query.length();
+ int parameterLength = parameter.length();
+
+ String value;
+ int index = 0;
+ while (true) {
+ index = query.indexOf(parameter, index);
+ if (index == -1) {
+ return null;
+ }
+
+ index += parameterLength;
+
+ if (queryLength == index) {
+ return null;
+ }
+
+ if (query.charAt(index) == '=') {
+ index++;
+ break;
+ }
+ }
+
+ int ampIndex = query.indexOf('&', index);
+ if (ampIndex == -1) {
+ value = query.substring(index);
+ } else {
+ value = query.substring(index, ampIndex);
+ }
+
+ return Uri.decode(value);
+ }
}
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index e939ef7..af2bd9e 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -20,7 +20,7 @@
import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import android.app.SearchManager;
@@ -32,11 +32,9 @@
import android.provider.Contacts.Intents;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Presence;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StatusUpdates;
import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
@@ -74,20 +72,11 @@
};
private interface SearchSuggestionQuery {
- public static final String JOIN_RAW_CONTACTS =
- " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) ";
-
- public static final String JOIN_CONTACTS =
- " JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
- public static final String JOIN_MIMETYPES =
- " JOIN mimetypes ON (data.mimetype_id = mimetypes._id AND mimetypes.mimetype IN ('"
- + StructuredName.CONTENT_ITEM_TYPE + "','" + Email.CONTENT_ITEM_TYPE + "','"
- + Phone.CONTENT_ITEM_TYPE + "','" + Organization.CONTENT_ITEM_TYPE + "','"
- + GroupMembership.CONTENT_ITEM_TYPE + "')) ";
-
- public static final String TABLE = "data " + JOIN_RAW_CONTACTS + JOIN_MIMETYPES
- + JOIN_CONTACTS;
+ public static final String TABLE = "data "
+ + " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ + " JOIN contacts ON (raw_contacts.contact_id = contacts._id)"
+ + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON ("
+ + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
public static final String PRESENCE_SQL =
"(SELECT " + StatusUpdates.PRESENCE_STATUS +
@@ -97,14 +86,13 @@
public static final String[] COLUMNS = {
ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID,
- ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + Contacts.DISPLAY_NAME,
+ "name_raw_contact." + RawContactsColumns.DISPLAY_NAME
+ + " AS " + Contacts.DISPLAY_NAME,
PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE,
DataColumns.CONCRETE_ID + " AS data_id",
- MimetypesColumns.MIMETYPE,
+ DataColumns.MIMETYPE_ID,
Data.IS_SUPER_PRIMARY,
- Organization.COMPANY,
- Email.DATA,
- Phone.NUMBER,
+ Data.DATA1,
Contacts.PHOTO_ID,
};
@@ -112,12 +100,12 @@
public static final int DISPLAY_NAME = 1;
public static final int PRESENCE_STATUS = 2;
public static final int DATA_ID = 3;
- public static final int MIMETYPE = 4;
+ public static final int MIMETYPE_ID = 4;
public static final int IS_SUPER_PRIMARY = 5;
public static final int ORGANIZATION = 6;
- public static final int EMAIL = 7;
- public static final int PHONE = 8;
- public static final int PHOTO_ID = 9;
+ public static final int EMAIL = 6;
+ public static final int PHONE = 6;
+ public static final int PHOTO_ID = 7;
}
private static class SearchSuggestion {
@@ -225,9 +213,34 @@
}
private final ContactsProvider2 mContactsProvider;
+ private boolean mMimeTypeIdsLoaded;
+ private long mMimeTypeIdEmail;
+ private long mMimeTypeIdStructuredName;
+ private long mMimeTypeIdOrganization;
+ private long mMimeTypeIdPhone;
+ @SuppressWarnings("all")
public GlobalSearchSupport(ContactsProvider2 contactsProvider) {
mContactsProvider = contactsProvider;
+
+ // To ensure the data column position. This is dead code if properly configured.
+ if (Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
+ || Email.DATA != Data.DATA1) {
+ throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
+ + " data is not in DATA1 column");
+ }
+ }
+
+ private void ensureMimetypeIdsLoaded() {
+ if (!mMimeTypeIdsLoaded) {
+ ContactsDatabaseHelper dbHelper = (ContactsDatabaseHelper)mContactsProvider
+ .getDatabaseHelper();
+ mMimeTypeIdStructuredName = dbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
+ mMimeTypeIdOrganization = dbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
+ mMimeTypeIdPhone = dbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+ mMimeTypeIdEmail = dbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
+ mMimeTypeIdsLoaded = true;
+ }
}
public Cursor handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String limit) {
@@ -243,11 +256,14 @@
}
}
- public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, long contactId, String[] projection) {
+ public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, long contactId,
+ String[] projection) {
+ ensureMimetypeIdsLoaded();
StringBuilder sb = new StringBuilder();
sb.append(mContactsProvider.getContactsRestrictions());
- sb.append(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
- return buildCursorForSearchSuggestions(db, sb.toString(), projection);
+ appendMimeTypeFilter(sb);
+ sb.append(" AND " + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + contactId);
+ return buildCursorForSearchSuggestions(db, sb.toString(), projection, null);
}
private Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) {
@@ -296,22 +312,46 @@
private Cursor buildCursorForSearchSuggestionsBasedOnName(SQLiteDatabase db,
String searchClause, String limit) {
-
+ ensureMimetypeIdsLoaded();
StringBuilder sb = new StringBuilder();
sb.append(mContactsProvider.getContactsRestrictions());
+ appendMimeTypeFilter(sb);
sb.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN ");
mContactsProvider.appendRawContactsByFilterAsNestedQuery(sb, searchClause, limit);
- sb.append(" AND " + Contacts.IN_VISIBLE_GROUP + "=1");
- return buildCursorForSearchSuggestions(db, sb.toString(), null);
+ /*
+ * Prepending "+" to the IN_VISIBLE_GROUP column disables the index on the
+ * that column. The logic is this: let's say we have 10,000 contacts
+ * of which 500 are visible. The first letter we type narrows this down
+ * to 10,000/26 = 384, which is already less than 500 that we would get
+ * from the IN_VISIBLE_GROUP index. Typing the second letter will narrow
+ * the search down to 10,000/26/26 = 14 contacts. And a lot of people
+ * will have more that 5% of their contacts visible, while the alphabet
+ * will always have 26 letters.
+ */
+ sb.append(" AND " + "+" + Contacts.IN_VISIBLE_GROUP + "=1");
+ String selection = sb.toString();
+
+ return buildCursorForSearchSuggestions(db, selection, null, limit);
}
- private Cursor buildCursorForSearchSuggestions(SQLiteDatabase db, String selection,
- String[] projection) {
+ private void appendMimeTypeFilter(StringBuilder sb) {
+
+ /*
+ * The "+" syntax prevents the mime type index from being used - we just want
+ * to reduce the size of the result set, not actually search by mime types.
+ */
+ sb.append(" AND " + "+" + DataColumns.MIMETYPE_ID + " IN (" + mMimeTypeIdEmail + "," +
+ mMimeTypeIdOrganization + "," + mMimeTypeIdPhone + "," +
+ mMimeTypeIdStructuredName + ")");
+ }
+
+ private Cursor buildCursorForSearchSuggestions(SQLiteDatabase db,
+ String selection, String[] projection, String limit) {
ArrayList<SearchSuggestion> suggestionList = new ArrayList<SearchSuggestion>();
HashMap<Long, SearchSuggestion> suggestionMap = new HashMap<Long, SearchSuggestion>();
- Cursor c = db.query(true, SearchSuggestionQuery.TABLE,
- SearchSuggestionQuery.COLUMNS, selection, null, null, null, null, null);
+ Cursor c = db.query(false, SearchSuggestionQuery.TABLE,
+ SearchSuggestionQuery.COLUMNS, selection, null, null, null, null, limit);
try {
while (c.moveToNext()) {
@@ -330,18 +370,18 @@
suggestion.presence = c.getInt(SearchSuggestionQuery.PRESENCE_STATUS);
}
- String mimetype = c.getString(SearchSuggestionQuery.MIMETYPE);
- if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ long mimetype = c.getLong(SearchSuggestionQuery.MIMETYPE_ID);
+ if (mimetype == mMimeTypeIdStructuredName) {
suggestion.titleIsName = true;
- } else if (Organization.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ } else if (mimetype == mMimeTypeIdOrganization) {
if (isSuperPrimary || suggestion.organization == null) {
suggestion.organization = c.getString(SearchSuggestionQuery.ORGANIZATION);
}
- } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ } else if (mimetype == mMimeTypeIdEmail) {
if (isSuperPrimary || suggestion.email == null) {
suggestion.email = c.getString(SearchSuggestionQuery.EMAIL);
}
- } else if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ } else if (mimetype == mMimeTypeIdPhone) {
if (isSuperPrimary || suggestion.phoneNumber == null) {
suggestion.phoneNumber = c.getString(SearchSuggestionQuery.PHONE);
}
diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java
index 65120f9..703230d 100644
--- a/src/com/android/providers/contacts/LegacyApiSupport.java
+++ b/src/com/android/providers/contacts/LegacyApiSupport.java
@@ -1531,7 +1531,7 @@
switch (match) {
case PEOPLE:
case PEOPLE_ID:
- count = mContactsProvider.deleteRawContact(id, false);
+ count = mContactsProvider.deleteRawContact(id, mDbHelper.getContactId(id), false);
break;
case PEOPLE_PHOTO:
diff --git a/src/com/android/providers/contacts/LegacyContactImporter.java b/src/com/android/providers/contacts/LegacyContactImporter.java
index 9c17e4a..2f63637 100644
--- a/src/com/android/providers/contacts/LegacyContactImporter.java
+++ b/src/com/android/providers/contacts/LegacyContactImporter.java
@@ -383,7 +383,7 @@
Contacts.SEND_TO_VOICEMAIL + "," +
Contacts.STARRED + "," +
Contacts.TIMES_CONTACTED + "," +
- Contacts.DISPLAY_NAME +
+ Contacts.NAME_RAW_CONTACT_ID +
") VALUES (?,?,?,?,?,?,?)";
int ID = 1;
@@ -392,7 +392,7 @@
int SEND_TO_VOICEMAIL = 4;
int STARRED = 5;
int TIMES_CONTACTED = 6;
- int DISPLAY_NAME = 7;
+ int NAME_RAW_CONTACT_ID = 7;
}
private interface StructuredNameInsert {
@@ -518,8 +518,7 @@
c.getLong(PeopleQuery.STARRED));
insert.bindLong(ContactsInsert.TIMES_CONTACTED,
c.getLong(PeopleQuery.TIMES_CONTACTED));
- bindString(insert, ContactsInsert.DISPLAY_NAME,
- c.getString(PeopleQuery.NAME));
+ insert.bindLong(ContactsInsert.NAME_RAW_CONTACT_ID, id);
insert(insert);
}
@@ -814,12 +813,14 @@
String INSERT_SQL = "INSERT INTO " + Tables.PHONE_LOOKUP + "(" +
PhoneLookupColumns.RAW_CONTACT_ID + "," +
PhoneLookupColumns.DATA_ID + "," +
- PhoneLookupColumns.NORMALIZED_NUMBER +
- ") VALUES (?,?,?)";
+ PhoneLookupColumns.NORMALIZED_NUMBER + "," +
+ PhoneLookupColumns.MIN_MATCH +
+ ") VALUES (?,?,?,?)";
int RAW_CONTACT_ID = 1;
int DATA_ID = 2;
int NORMALIZED_NUMBER = 3;
+ int MIN_MATCH = 4;
}
private interface HasPhoneNumberUpdate {
@@ -868,6 +869,8 @@
phoneLookupInsert.bindLong(PhoneLookupInsert.RAW_CONTACT_ID, id);
phoneLookupInsert.bindLong(PhoneLookupInsert.DATA_ID, dataId);
phoneLookupInsert.bindString(PhoneLookupInsert.NORMALIZED_NUMBER, normalizedNumber);
+ phoneLookupInsert.bindString(PhoneLookupInsert.MIN_MATCH,
+ PhoneNumberUtils.toCallerIDMinMatch(number));
insert(phoneLookupInsert);
if (lastUpdatedContact != id) {
diff --git a/src/com/android/providers/contacts/SQLiteContentProvider.java b/src/com/android/providers/contacts/SQLiteContentProvider.java
index 4d1ec90..56903bf 100644
--- a/src/com/android/providers/contacts/SQLiteContentProvider.java
+++ b/src/com/android/providers/contacts/SQLiteContentProvider.java
@@ -44,6 +44,11 @@
private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
+ /**
+ * Maximum number of operations allowed in a batch between yield points.
+ */
+ private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
+
@Override
public boolean onCreate() {
Context context = getContext();
@@ -186,6 +191,8 @@
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
+ int ypCount = 0;
+ int opCount = 0;
mDb = mOpenHelper.getWritableDatabase();
mDb.beginTransactionWithListener(this);
try {
@@ -193,9 +200,18 @@
final int numOperations = operations.size();
final ContentProviderResult[] results = new ContentProviderResult[numOperations];
for (int i = 0; i < numOperations; i++) {
+ if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
+ throw new OperationApplicationException(
+ "Too many content provider operations between yield points. "
+ + "The maximum number of operations per yield point is "
+ + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
+ }
final ContentProviderOperation operation = operations.get(i);
if (i > 0 && operation.isYieldAllowed()) {
- mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY);
+ opCount = 0;
+ if (mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
+ ypCount++;
+ }
}
results[i] = operation.apply(this, results, i);
}
diff --git a/src/com/android/providers/contacts/SocialProvider.java b/src/com/android/providers/contacts/SocialProvider.java
index 349e1fc..8201d90 100644
--- a/src/com/android/providers/contacts/SocialProvider.java
+++ b/src/com/android/providers/contacts/SocialProvider.java
@@ -84,7 +84,8 @@
// Contacts projection map
columns = new HashMap<String, String>();
- columns.put(Contacts.DISPLAY_NAME, ContactsColumns.CONCRETE_DISPLAY_NAME + " AS "
+ // TODO: fix display name reference (in fact, use the contacts view instead of the table)
+ columns.put(Contacts.DISPLAY_NAME, "contact." + Contacts.DISPLAY_NAME + " AS "
+ Contacts.DISPLAY_NAME);
sContactsProjectionMap = columns;
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index bcc7f6e..2117829 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -391,7 +391,6 @@
}
protected void assertAggregated(long rawContactId1, long rawContactId2) {
- forceAggregation();
long contactId1 = queryContactId(rawContactId1);
long contactId2 = queryContactId(rawContactId2);
assertTrue(contactId1 == contactId2);
@@ -399,8 +398,6 @@
protected void assertAggregated(long rawContactId1, long rawContactId2,
String expectedDisplayName) {
- forceAggregation();
-
long contactId1 = queryContactId(rawContactId1);
long contactId2 = queryContactId(rawContactId2);
assertTrue(contactId1 == contactId2);
@@ -410,8 +407,6 @@
}
protected void assertNotAggregated(long rawContactId1, long rawContactId2) {
- forceAggregation();
-
long contactId1 = queryContactId(rawContactId1);
long contactId2 = queryContactId(rawContactId2);
assertTrue(contactId1 != contactId2);
@@ -816,10 +811,6 @@
c.close();
}
- protected void forceAggregation() {
- ((SynchronousContactsProvider2) mActor.provider).aggregate();
- }
-
protected void assertNetworkNotified(boolean expected) {
assertEquals(expected, ((SynchronousContactsProvider2)mActor.provider).isNetworkNotified());
}
diff --git a/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java b/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java
deleted file mode 100644
index 6c15e0e..0000000
--- a/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source 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.providers.contacts;
-
-import android.test.suitebuilder.annotation.SmallTest;
-
-import junit.framework.TestCase;
-
-/**
- * Tests from {@link ContactAggregationScheduler}.
- *
- * Run the test like this:
- * <code>
- * adb shell am instrument -e class com.android.providers.contacts.ContactAggregationSchedulerTest \
- * -w com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
- * </code>
- */
-@SmallTest
-public class ContactAggregationSchedulerTest extends TestCase {
-
- private TestContactAggregationScheduler mScheduler;
-
- @Override
- protected void setUp() throws Exception {
- mScheduler = new TestContactAggregationScheduler();
- }
-
- public void testScheduleInitial() {
- mScheduler.schedule();
- assertEquals(1, mScheduler.mRunNow);
- assertEquals(0, mScheduler.mRunDelayed);
- }
-
- public void testScheduleTwiceRapidly() {
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
- mScheduler.schedule();
- assertEquals(1, mScheduler.mRunNow);
- assertEquals(1, mScheduler.mRunDelayed);
- }
-
- public void testScheduleThriceRapidly() {
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.AGGREGATION_DELAY / 2;
- mScheduler.schedule();
-
- assertEquals(1, mScheduler.mRunNow);
- assertEquals(2, mScheduler.mRunDelayed);
- }
-
- public void testScheduleThriceExceedingMaxDelay() {
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
- mScheduler.schedule();
- assertEquals(1, mScheduler.mRunDelayed);
- }
-
- public void testScheduleWhileRunning() {
- mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
- boolean mInterruptCalled;
-
- public void interrupt() {
- mInterruptCalled = true;
- }
-
- public void run() {
- mScheduler.schedule();
- assertTrue(mInterruptCalled);
- }
- });
-
- mScheduler.run();
- assertEquals(1, mScheduler.mRunDelayed);
- }
-
- public void testRepeatedInterruptions() {
- mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
- boolean mInterruptCalled;
-
- public void interrupt() {
- mInterruptCalled = true;
- }
-
- public void run() {
- mScheduler.schedule();
- assertTrue(mInterruptCalled);
- }
- });
-
- mScheduler.run();
- assertEquals(1, mScheduler.mRunDelayed);
-
- mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
- mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
- boolean mInterruptCalled;
-
- public void interrupt() {
- mInterruptCalled = true;
- }
-
- public void run() {
- mScheduler.schedule();
- assertFalse(mInterruptCalled);
- }
- });
-
- mScheduler.run();
- }
-
- public void testScheduleWhileRunningExceedingMaxDelay() {
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
-
- mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
- boolean mInterruptCalled;
-
- public void interrupt() {
- mInterruptCalled = true;
- }
-
- public void run() {
- mScheduler.schedule();
- assertFalse(mInterruptCalled);
- }
- });
-
- mScheduler.run();
- assertEquals(1, mScheduler.mRunNow);
- assertEquals(2, mScheduler.mRunDelayed);
- }
-
- private static class TestContactAggregationScheduler extends ContactAggregationScheduler {
-
- long mTime = 1000;
- int mRunDelayed;
- int mRunNow;
-
- @Override
- public void start() {
- }
-
- @Override
- public void stop() {
- }
-
- @Override
- long currentTime() {
- return mTime;
- }
-
- @Override
- void runNow() {
- mRunNow++;
- }
-
- @Override
- void runDelayed() {
- mRunDelayed++;
- }
- }
-}
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
index e4dc629..a71c58b 100644
--- a/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
@@ -95,7 +95,10 @@
if (TRACE) {
Debug.startMethodTracing("aggregation");
}
- provider.aggregate();
+
+ // TODO
+// provider.aggregate();
+
if (TRACE) {
Debug.stopMethodTracing();
}
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
index 04b8c47..f864641 100644
--- a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
@@ -22,6 +22,7 @@
import android.net.Uri;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.test.suitebuilder.annotation.LargeTest;
@@ -368,6 +369,20 @@
assertNotAggregated(rawContactId1, rawContactId2);
}
+ public void testNonAggregationOnOrganization() {
+ ContentValues values = new ContentValues();
+ values.put(Organization.TITLE, "Monsters, Inc");
+ long rawContactId1 = createRawContact();
+ insertOrganization(rawContactId1, values);
+ insertNickname(rawContactId1, "Boo");
+
+ long rawContactId2 = createRawContact();
+ insertOrganization(rawContactId2, values);
+ insertNickname(rawContactId2, "Rendall"); // To force reaggregation
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
public void testAggregationExceptionKeepIn() {
long rawContactId1 = createRawContact();
insertStructuredName(rawContactId1, "Johnk", "Smithk");
@@ -627,8 +642,6 @@
}
private void assertSuggestions(long contactId, long... suggestions) {
- forceAggregation();
-
final Uri aggregateUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
Uri uri = Uri.withAppendedPath(aggregateUri,
Contacts.AggregationSuggestions.CONTENT_DIRECTORY);
diff --git a/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java b/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
index 1d57924..390b940 100644
--- a/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
+++ b/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
@@ -126,8 +126,6 @@
long rawContactId3 = createRawContactWithName("John", "Doe");
storeValue(RawContacts.CONTENT_URI, rawContactId3, RawContacts.SOURCE_ID, "3");
- forceAggregation();
-
String lookupKey = "0i1.0i2.0i3";
long contactId = queryContactId(rawContactId1);
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index afeb377..1c175a4 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -18,8 +18,11 @@
import com.android.internal.util.ArrayUtils;
import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
+import com.google.android.collect.Lists;
import android.accounts.Account;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Entity;
@@ -174,8 +177,6 @@
long rawContactId2 = createRawContactWithName("Hot", "Tamale");
insertPhoneNumber(rawContactId2, "1-800-466-4411");
- forceAggregation();
-
Uri filterUri1 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "tamale");
ContentValues values = new ContentValues();
values.put(Contacts.DISPLAY_NAME, "Hot Tamale");
@@ -322,7 +323,6 @@
long rawContactId2 = createRawContactWithName("Hot", "Tamale");
insertEmail(rawContactId2, "tamale@acme.com");
- forceAggregation();
Uri filterUri1 = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "tam");
ContentValues values = new ContentValues();
@@ -679,6 +679,23 @@
assertStoredValue(uri, Contacts.DISPLAY_NAME, "Dog");
}
+ public void testInsertDataWithContentProviderOperations() throws Exception {
+ ContentProviderOperation cpo1 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValues(new ContentValues())
+ .build();
+ ContentProviderOperation cpo2 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+ .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(StructuredName.GIVEN_NAME, "John")
+ .withValue(StructuredName.FAMILY_NAME, "Doe")
+ .build();
+ ContentProviderResult[] results =
+ mResolver.applyBatch(ContactsContract.AUTHORITY, Lists.newArrayList(cpo1, cpo2));
+ long contactId = queryContactId(ContentUris.parseId(results[0].uri));
+ Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ assertStoredValue(uri, Contacts.DISPLAY_NAME, "John Doe");
+ }
+
public void testSendToVoicemailDefault() {
long rawContactId = createRawContactWithName();
long contactId = queryContactId(rawContactId);
@@ -894,11 +911,53 @@
values.put(Contacts.CONTACT_STATUS, "Available");
assertStoredValuesWithProjection(contactUri, values);
- mResolver.delete(StatusUpdates.CONTENT_URI, StatusUpdates.DATA_ID + "=" + statusId, null);
- values.putNull(Contacts.CONTACT_PRESENCE);
+ // update status_updates table to set new values for
+ // status_updates.status
+ // status_updates.status_ts
+ // presence
+ long updatedTs = 200;
+ String testUpdate = "test_update";
+ String selection = StatusUpdates.DATA_ID + "=" + statusId;
+ values.clear();
+ values.put(StatusUpdates.STATUS_TIMESTAMP, updatedTs);
+ values.put(StatusUpdates.STATUS, testUpdate);
+ values.put(StatusUpdates.PRESENCE, "presence_test");
+ mResolver.update(StatusUpdates.CONTENT_URI, values,
+ StatusUpdates.DATA_ID + "=" + statusId, null);
+ assertStoredValuesWithProjection(StatusUpdates.CONTENT_URI, values);
- // Latest custom status update stays on the phone
- values.put(Contacts.CONTACT_STATUS, "Available");
+ // update status_updates table to set new values for columns in status_updates table ONLY
+ // i.e., no rows in presence table are to be updated.
+ updatedTs = 300;
+ testUpdate = "test_update_new";
+ selection = StatusUpdates.DATA_ID + "=" + statusId;
+ values.clear();
+ values.put(StatusUpdates.STATUS_TIMESTAMP, updatedTs);
+ values.put(StatusUpdates.STATUS, testUpdate);
+ mResolver.update(StatusUpdates.CONTENT_URI, values,
+ StatusUpdates.DATA_ID + "=" + statusId, null);
+ // make sure the presence column value is still the old value
+ values.put(StatusUpdates.PRESENCE, "presence_test");
+ assertStoredValuesWithProjection(StatusUpdates.CONTENT_URI, values);
+
+ // update status_updates table to set new values for columns in presence table ONLY
+ // i.e., no rows in status_updates table are to be updated.
+ selection = StatusUpdates.DATA_ID + "=" + statusId;
+ values.clear();
+ values.put(StatusUpdates.PRESENCE, "presence_test_new");
+ mResolver.update(StatusUpdates.CONTENT_URI, values,
+ StatusUpdates.DATA_ID + "=" + statusId, null);
+ // make sure the status_updates table is not updated
+ values.put(StatusUpdates.STATUS_TIMESTAMP, updatedTs);
+ values.put(StatusUpdates.STATUS, testUpdate);
+ assertStoredValuesWithProjection(StatusUpdates.CONTENT_URI, values);
+
+ // effect "delete status_updates" operation and expect the following
+ // data deleted from status_updates table
+ // presence set to null
+ mResolver.delete(StatusUpdates.CONTENT_URI, StatusUpdates.DATA_ID + "=" + statusId, null);
+ values.clear();
+ values.putNull(Contacts.CONTACT_PRESENCE);
assertStoredValuesWithProjection(contactUri, values);
}
@@ -1334,7 +1393,6 @@
public void testContactDeletion() {
long rawContactId1 = createRawContactWithName("John", "Doe");
long rawContactId2 = createRawContactWithName("John", "Doe");
- forceAggregation();
long contactId = queryContactId(rawContactId1);
@@ -1628,6 +1686,42 @@
c.close();
}
+ public void testReadBooleanQueryParameter() {
+ assertBooleanUriParameter("foo:bar", "bool", true, true);
+ assertBooleanUriParameter("foo:bar", "bool", false, false);
+ assertBooleanUriParameter("foo:bar?bool=0", "bool", true, false);
+ assertBooleanUriParameter("foo:bar?bool=1", "bool", false, true);
+ assertBooleanUriParameter("foo:bar?bool=false", "bool", true, false);
+ assertBooleanUriParameter("foo:bar?bool=true", "bool", false, true);
+ assertBooleanUriParameter("foo:bar?bool=FaLsE", "bool", true, false);
+ assertBooleanUriParameter("foo:bar?bool=false&some=some", "bool", true, false);
+ assertBooleanUriParameter("foo:bar?bool=1&some=some", "bool", false, true);
+ assertBooleanUriParameter("foo:bar?some=bool", "bool", true, true);
+ assertBooleanUriParameter("foo:bar?bool", "bool", true, true);
+ }
+
+ private void assertBooleanUriParameter(String uriString, String parameter,
+ boolean defaultValue, boolean expectedValue) {
+ assertEquals(expectedValue, ContactsProvider2.readBooleanQueryParameter(
+ Uri.parse(uriString), parameter, defaultValue));
+ }
+
+ public void testGetQueryParameter() {
+ assertQueryParameter("foo:bar", "param", null);
+ assertQueryParameter("foo:bar?param", "param", null);
+ assertQueryParameter("foo:bar?param=", "param", "");
+ assertQueryParameter("foo:bar?param=val", "param", "val");
+ assertQueryParameter("foo:bar?param=val&some=some", "param", "val");
+ assertQueryParameter("foo:bar?some=some¶m=val", "param", "val");
+ assertQueryParameter("foo:bar?some=some¶m=val&else=else", "param", "val");
+ assertQueryParameter("foo:bar?param=john%40doe.com", "param", "john@doe.com");
+ }
+
+ private void assertQueryParameter(String uriString, String parameter, String expectedValue) {
+ assertEquals(expectedValue, ContactsProvider2.getQueryParameter(
+ Uri.parse(uriString), parameter));
+ }
+
private long createContact(ContentValues values, String firstName, String givenName,
String phoneNumber, String email, int presenceStatus, int timesContacted, int starred,
long groupId) {
diff --git a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java b/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
index bdfab7a..3919770 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
@@ -77,8 +77,7 @@
targetContext, "perf_imp.");
targetContextWrapper.makeExistingFilesAndDbsAccessible();
IsolatedContext providerContext = new IsolatedContext(resolver, targetContextWrapper);
- TestAggregationScheduler scheduler = new TestAggregationScheduler();
- SynchronousContactsProvider2 provider = new SynchronousContactsProvider2(scheduler);
+ SynchronousContactsProvider2 provider = new SynchronousContactsProvider2();
provider.setDataWipeEnabled(false);
provider.attachInfo(providerContext, null);
resolver.addProvider(ContactsContract.AUTHORITY, provider);
@@ -120,28 +119,4 @@
return mDbHelper;
}
}
-
- private static class TestAggregationScheduler extends ContactAggregationScheduler {
-
- @Override
- public void start() {
- }
-
- @Override
- public void stop() {
- }
-
- @Override
- long currentTime() {
- return 0;
- }
-
- @Override
- void runDelayed() {
- }
-
- void trigger() {
- run();
- }
- }
}
diff --git a/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java b/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
index af15b3d..08c7582 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
@@ -184,8 +184,6 @@
Calls.CACHED_NUMBER_TYPE,
});
- // Performing an aggregation pass should not change anything.
- provider.scheduleContactAggregation();
assertQueryResults("expected_contacts.txt", Contacts.CONTENT_URI, new String[]{
Contacts._ID,
Contacts.DISPLAY_NAME,
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index f5a3572..12b809d 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -31,14 +31,6 @@
private Account mAccount;
private boolean mNetworkNotified;
- public SynchronousContactsProvider2() {
- this(new SynchronousAggregationScheduler());
- }
-
- public SynchronousContactsProvider2(ContactAggregationScheduler scheduler) {
- super(scheduler);
- }
-
@Override
protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
if (mDbHelper == null) {
@@ -135,31 +127,4 @@
// We have an explicit test for data conversion - no need to do it every time
return false;
}
-
- public void aggregate() {
- ContactAggregationScheduler scheduler = getContactAggregationScheduler();
- scheduler.run();
- }
-
- private static class SynchronousAggregationScheduler extends ContactAggregationScheduler {
-
- @Override
- public void start() {
- }
-
- @Override
- public void stop() {
- }
-
- @Override
- long currentTime() {
- return 0;
- }
-
- @Override
- void runDelayed() {
-// super.run();
- }
-
- }
}