• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.providers.contacts;
18 
19 import com.android.providers.contacts.ContactMatcher.MatchScore;
20 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
21 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
22 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
23 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
24 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
25 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
26 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
27 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
28 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
29 
30 import android.content.ContentValues;
31 import android.database.Cursor;
32 import android.database.DatabaseUtils;
33 import android.database.sqlite.SQLiteDatabase;
34 import android.database.sqlite.SQLiteQueryBuilder;
35 import android.database.sqlite.SQLiteStatement;
36 import android.net.Uri;
37 import android.provider.ContactsContract.AggregationExceptions;
38 import android.provider.ContactsContract.Contacts;
39 import android.provider.ContactsContract.Data;
40 import android.provider.ContactsContract.DisplayNameSources;
41 import android.provider.ContactsContract.RawContacts;
42 import android.provider.ContactsContract.StatusUpdates;
43 import android.provider.ContactsContract.CommonDataKinds.Email;
44 import android.provider.ContactsContract.CommonDataKinds.Phone;
45 import android.provider.ContactsContract.CommonDataKinds.Photo;
46 import android.text.TextUtils;
47 import android.util.EventLog;
48 import android.util.Log;
49 
50 import java.util.ArrayList;
51 import java.util.Collections;
52 import java.util.HashMap;
53 import java.util.HashSet;
54 import java.util.Iterator;
55 import java.util.List;
56 
57 
58 /**
59  * ContactAggregator deals with aggregating contact information coming from different sources.
60  * Two John Doe contacts from two disjoint sources are presumed to be the same
61  * person unless the user declares otherwise.
62  */
63 public class ContactAggregator {
64 
65     private static final String TAG = "ContactAggregator";
66 
67     private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
68 
69     private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL =
70             NameLookupColumns.NAME_TYPE + " IN ("
71                     + NameLookupType.NAME_EXACT + ","
72                     + NameLookupType.NAME_VARIANT + ","
73                     + NameLookupType.NAME_COLLATION_KEY + ")";
74 
75     // From system/core/logcat/event-log-tags
76     // aggregator [time, count] will be logged for each aggregator cycle.
77     // For the query (as opposed to the merge), count will be negative
78     public static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747;
79 
80     // If we encounter more than this many contacts with matching names, aggregate only this many
81     private static final int PRIMARY_HIT_LIMIT = 15;
82     private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT);
83 
84     // If we encounter more than this many contacts with matching phone number or email,
85     // don't attempt to aggregate - this is likely an error or a shared corporate data element.
86     private static final int SECONDARY_HIT_LIMIT = 20;
87     private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT);
88 
89     // If we encounter more than this many contacts with matching name during aggregation
90     // suggestion lookup, ignore the remaining results.
91     private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100;
92 
93     private final ContactsProvider2 mContactsProvider;
94     private final ContactsDatabaseHelper mDbHelper;
95     private PhotoPriorityResolver mPhotoPriorityResolver;
96     private boolean mEnabled = true;
97 
98     /** Precompiled sql statement for setting an aggregated presence */
99     private SQLiteStatement mAggregatedPresenceReplace;
100     private SQLiteStatement mPresenceContactIdUpdate;
101     private SQLiteStatement mRawContactCountQuery;
102     private SQLiteStatement mContactDelete;
103     private SQLiteStatement mAggregatedPresenceDelete;
104     private SQLiteStatement mMarkForAggregation;
105     private SQLiteStatement mPhotoIdUpdate;
106     private SQLiteStatement mDisplayNameUpdate;
107     private SQLiteStatement mHasPhoneNumberUpdate;
108     private SQLiteStatement mLookupKeyUpdate;
109     private SQLiteStatement mStarredUpdate;
110     private SQLiteStatement mContactIdAndMarkAggregatedUpdate;
111     private SQLiteStatement mContactIdUpdate;
112     private SQLiteStatement mMarkAggregatedUpdate;
113     private SQLiteStatement mContactUpdate;
114     private SQLiteStatement mContactInsert;
115 
116     private HashMap<Long, Integer> mRawContactsMarkedForAggregation = new HashMap<Long, Integer>();
117 
118     private String[] mSelectionArgs1 = new String[1];
119     private String[] mSelectionArgs2 = new String[2];
120     private String[] mSelectionArgs3 = new String[3];
121     private long mMimeTypeIdEmail;
122     private long mMimeTypeIdPhoto;
123     private long mMimeTypeIdPhone;
124     private String mRawContactsQueryByRawContactId;
125     private String mRawContactsQueryByContactId;
126     private StringBuilder mSb = new StringBuilder();
127     private MatchCandidateList mCandidates = new MatchCandidateList();
128     private ContactMatcher mMatcher = new ContactMatcher();
129     private ContentValues mValues = new ContentValues();
130     private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate();
131 
132     /**
133      * Captures a potential match for a given name. The matching algorithm
134      * constructs a bunch of NameMatchCandidate objects for various potential matches
135      * and then executes the search in bulk.
136      */
137     private static class NameMatchCandidate {
138         String mName;
139         int mLookupType;
140 
NameMatchCandidate(String name, int nameLookupType)141         public NameMatchCandidate(String name, int nameLookupType) {
142             mName = name;
143             mLookupType = nameLookupType;
144         }
145     }
146 
147     /**
148      * A list of {@link NameMatchCandidate} that keeps its elements even when the list is
149      * truncated. This is done for optimization purposes to avoid excessive object allocation.
150      */
151     private static class MatchCandidateList {
152         private final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>();
153         private int mCount;
154 
155         /**
156          * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists.
157          */
add(String name, int nameLookupType)158         public void add(String name, int nameLookupType) {
159             if (mCount >= mList.size()) {
160                 mList.add(new NameMatchCandidate(name, nameLookupType));
161             } else {
162                 NameMatchCandidate candidate = mList.get(mCount);
163                 candidate.mName = name;
164                 candidate.mLookupType = nameLookupType;
165             }
166             mCount++;
167         }
168 
clear()169         public void clear() {
170             mCount = 0;
171         }
172     }
173 
174     /**
175      * A convenience class used in the algorithm that figures out which of available
176      * display names to use for an aggregate contact.
177      */
178     private static class DisplayNameCandidate {
179         long rawContactId;
180         String displayName;
181         int displayNameSource;
182         boolean verified;
183         boolean writableAccount;
184 
DisplayNameCandidate()185         public DisplayNameCandidate() {
186             clear();
187         }
188 
clear()189         public void clear() {
190             rawContactId = -1;
191             displayName = null;
192             displayNameSource = DisplayNameSources.UNDEFINED;
193             verified = false;
194             writableAccount = false;
195         }
196     }
197 
198     /**
199      * Constructor.
200      */
ContactAggregator(ContactsProvider2 contactsProvider, ContactsDatabaseHelper contactsDatabaseHelper, PhotoPriorityResolver photoPriorityResolver)201     public ContactAggregator(ContactsProvider2 contactsProvider,
202             ContactsDatabaseHelper contactsDatabaseHelper,
203             PhotoPriorityResolver photoPriorityResolver) {
204         mContactsProvider = contactsProvider;
205         mDbHelper = contactsDatabaseHelper;
206         mPhotoPriorityResolver = photoPriorityResolver;
207 
208         SQLiteDatabase db = mDbHelper.getReadableDatabase();
209 
210         // Since we have no way of determining which custom status was set last,
211         // we'll just pick one randomly.  We are using MAX as an approximation of randomness
212         final String replaceAggregatePresenceSql =
213                 "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
214                 + AggregatedPresenceColumns.CONTACT_ID + ", "
215                 + StatusUpdates.PRESENCE_STATUS + ", "
216                 + StatusUpdates.CHAT_CAPABILITY + ")"
217                 + " SELECT " + PresenceColumns.CONTACT_ID + ","
218                 + StatusUpdates.PRESENCE_STATUS + ","
219                 + StatusUpdates.CHAT_CAPABILITY
220                 + " FROM " + Tables.PRESENCE
221                 + " WHERE "
222                 + " (" + StatusUpdates.PRESENCE_STATUS
223                 +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
224                 + " = (SELECT "
225                 + "MAX (" + StatusUpdates.PRESENCE_STATUS
226                 +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
227                 + " FROM " + Tables.PRESENCE
228                 + " WHERE " + PresenceColumns.CONTACT_ID
229                 + "=?)"
230                 + " AND " + PresenceColumns.CONTACT_ID
231                 + "=?;";
232         mAggregatedPresenceReplace = db.compileStatement(replaceAggregatePresenceSql);
233 
234         mRawContactCountQuery = db.compileStatement(
235                 "SELECT COUNT(" + RawContacts._ID + ")" +
236                 " FROM " + Tables.RAW_CONTACTS +
237                 " WHERE " + RawContacts.CONTACT_ID + "=?"
238                         + " AND " + RawContacts._ID + "<>?");
239 
240         mContactDelete = db.compileStatement(
241                 "DELETE FROM " + Tables.CONTACTS +
242                 " WHERE " + Contacts._ID + "=?");
243 
244         mAggregatedPresenceDelete = db.compileStatement(
245                 "DELETE FROM " + Tables.AGGREGATED_PRESENCE +
246                 " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?");
247 
248         mMarkForAggregation = db.compileStatement(
249                 "UPDATE " + Tables.RAW_CONTACTS +
250                 " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" +
251                 " WHERE " + RawContacts._ID + "=?"
252                         + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0");
253 
254         mPhotoIdUpdate = db.compileStatement(
255                 "UPDATE " + Tables.CONTACTS +
256                 " SET " + Contacts.PHOTO_ID + "=? " +
257                 " WHERE " + Contacts._ID + "=?");
258 
259         mDisplayNameUpdate = db.compileStatement(
260                 "UPDATE " + Tables.CONTACTS +
261                 " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " +
262                 " WHERE " + Contacts._ID + "=?");
263 
264         mLookupKeyUpdate = db.compileStatement(
265                 "UPDATE " + Tables.CONTACTS +
266                 " SET " + Contacts.LOOKUP_KEY + "=? " +
267                 " WHERE " + Contacts._ID + "=?");
268 
269         mHasPhoneNumberUpdate = db.compileStatement(
270                 "UPDATE " + Tables.CONTACTS +
271                 " SET " + Contacts.HAS_PHONE_NUMBER + "="
272                         + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)"
273                         + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS
274                         + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
275                                 + " AND " + Phone.NUMBER + " NOT NULL"
276                                 + " AND " + RawContacts.CONTACT_ID + "=?)" +
277                 " WHERE " + Contacts._ID + "=?");
278 
279         mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
280                 + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED
281                 + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE "
282                 + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND "
283                 + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?");
284 
285         mContactIdAndMarkAggregatedUpdate = db.compileStatement(
286                 "UPDATE " + Tables.RAW_CONTACTS +
287                 " SET " + RawContacts.CONTACT_ID + "=?, "
288                         + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
289                 " WHERE " + RawContacts._ID + "=?");
290 
291         mContactIdUpdate = db.compileStatement(
292                 "UPDATE " + Tables.RAW_CONTACTS +
293                 " SET " + RawContacts.CONTACT_ID + "=?" +
294                 " WHERE " + RawContacts._ID + "=?");
295 
296         mMarkAggregatedUpdate = db.compileStatement(
297                 "UPDATE " + Tables.RAW_CONTACTS +
298                 " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
299                 " WHERE " + RawContacts._ID + "=?");
300 
301         mPresenceContactIdUpdate = db.compileStatement(
302                 "UPDATE " + Tables.PRESENCE +
303                 " SET " + PresenceColumns.CONTACT_ID + "=?" +
304                 " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?");
305 
306         mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL);
307         mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL);
308 
309         mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
310         mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
311         mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
312 
313         // Query used to retrieve data from raw contacts to populate the corresponding aggregate
314         mRawContactsQueryByRawContactId = String.format(
315                 RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID,
316                 mMimeTypeIdPhoto, mMimeTypeIdPhone);
317 
318         mRawContactsQueryByContactId = String.format(
319                 RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
320                 mMimeTypeIdPhoto, mMimeTypeIdPhone);
321     }
322 
setEnabled(boolean enabled)323     public void setEnabled(boolean enabled) {
324         mEnabled = enabled;
325     }
326 
isEnabled()327     public boolean isEnabled() {
328         return mEnabled;
329     }
330 
331     private interface AggregationQuery {
332         String SQL =
333                 "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
334                         ", " + RawContacts.ACCOUNT_TYPE + "," + RawContacts.ACCOUNT_NAME +
335                 " FROM " + Tables.RAW_CONTACTS +
336                 " WHERE " + RawContacts._ID + " IN(";
337 
338         int _ID = 0;
339         int CONTACT_ID = 1;
340         int ACCOUNT_TYPE = 2;
341         int ACCOUNT_NAME = 3;
342     }
343 
344     /**
345      * Aggregate all raw contacts that were marked for aggregation in the current transaction.
346      * Call just before committing the transaction.
347      */
aggregateInTransaction(SQLiteDatabase db)348     public void aggregateInTransaction(SQLiteDatabase db) {
349         int count = mRawContactsMarkedForAggregation.size();
350         if (count == 0) {
351             return;
352         }
353 
354         long start = System.currentTimeMillis();
355         if (VERBOSE_LOGGING) {
356             Log.v(TAG, "Contact aggregation: " + count);
357         }
358 
359         EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -count);
360 
361         String selectionArgs[] = new String[count];
362 
363         int index = 0;
364         mSb.setLength(0);
365         mSb.append(AggregationQuery.SQL);
366         for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) {
367             if (index > 0) {
368                 mSb.append(',');
369             }
370             mSb.append('?');
371             selectionArgs[index++] = String.valueOf(rawContactId);
372         }
373 
374         mSb.append(')');
375 
376         long rawContactIds[] = new long[count];
377         long contactIds[] = new long[count];
378         String accountTypes[] = new String[count];
379         String accountNames[] = new String[count];
380         Cursor c = db.rawQuery(mSb.toString(), selectionArgs);
381         try {
382             count = c.getCount();
383             index = 0;
384             while (c.moveToNext()) {
385                 rawContactIds[index] = c.getLong(AggregationQuery._ID);
386                 contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
387                 accountTypes[index] = c.getString(AggregationQuery.ACCOUNT_TYPE);
388                 accountNames[index] = c.getString(AggregationQuery.ACCOUNT_NAME);
389                 index++;
390             }
391         } finally {
392             c.close();
393         }
394 
395         for (int i = 0; i < count; i++) {
396             aggregateContact(db, rawContactIds[i], accountTypes[i], accountNames[i], contactIds[i],
397                     mCandidates, mMatcher, mValues);
398         }
399 
400         long elapsedTime = System.currentTimeMillis() - start;
401         EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, count);
402 
403         if (VERBOSE_LOGGING) {
404             String performance = count == 0 ? "" : ", " + (elapsedTime / count) + " ms per contact";
405             Log.i(TAG, "Contact aggregation complete: " + count + performance);
406         }
407     }
408 
clearPendingAggregations()409     public void clearPendingAggregations() {
410         mRawContactsMarkedForAggregation.clear();
411     }
412 
markNewForAggregation(long rawContactId, int aggregationMode)413     public void markNewForAggregation(long rawContactId, int aggregationMode) {
414         mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode);
415     }
416 
markForAggregation(long rawContactId, int aggregationMode, boolean force)417     public void markForAggregation(long rawContactId, int aggregationMode, boolean force) {
418         if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) {
419             // As per ContactsContract documentation, default aggregation mode
420             // does not override a previously set mode
421             if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
422                 aggregationMode = mRawContactsMarkedForAggregation.get(rawContactId);
423             }
424         } else {
425             mMarkForAggregation.bindLong(1, rawContactId);
426             mMarkForAggregation.execute();
427         }
428 
429         mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode);
430     }
431 
432     /**
433      * Creates a new contact based on the given raw contact.  Does not perform aggregation.
434      */
onRawContactInsert(SQLiteDatabase db, long rawContactId)435     public void onRawContactInsert(SQLiteDatabase db, long rawContactId) {
436         mSelectionArgs1[0] = String.valueOf(rawContactId);
437         computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
438         long contactId = mContactInsert.executeInsert();
439         setContactId(rawContactId, contactId);
440         mDbHelper.updateContactVisible(contactId);
441     }
442 
443     private static final class RawContactIdAndAccountQuery {
444         public static final String TABLE = Tables.RAW_CONTACTS;
445 
446         public static final String[] COLUMNS = {
447                 RawContacts.CONTACT_ID, RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_NAME };
448 
449         public static final String SELECTION = RawContacts._ID + "=?";
450 
451         public static final int CONTACT_ID = 0;
452         public static final int ACCOUNT_TYPE = 1;
453         public static final int ACCOUNT_NAME = 2;
454     }
455 
aggregateContact(SQLiteDatabase db, long rawContactId)456     public void aggregateContact(SQLiteDatabase db, long rawContactId) {
457         long contactId = 0;
458         String accountName = null;
459         String accountType = null;
460         mSelectionArgs1[0] = String.valueOf(rawContactId);
461         Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE,
462                 RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION,
463                 mSelectionArgs1, null, null, null);
464         try {
465             if (cursor.moveToFirst()) {
466                 contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID);
467                 accountType = cursor.getString(RawContactIdAndAccountQuery.ACCOUNT_TYPE);
468                 accountName = cursor.getString(RawContactIdAndAccountQuery.ACCOUNT_NAME);
469             }
470         } finally {
471             cursor.close();
472         }
473         aggregateContact(db, rawContactId, accountType, accountName, contactId);
474     }
475 
476     /**
477      * Synchronously aggregate the specified contact assuming an open transaction.
478      */
aggregateContact(SQLiteDatabase db, long rawContactId, String accountType, String accountName, long currentContactId)479     public void aggregateContact(SQLiteDatabase db, long rawContactId, String accountType,
480             String accountName, long currentContactId) {
481         if (!mEnabled) {
482             return;
483         }
484 
485         MatchCandidateList candidates = new MatchCandidateList();
486         ContactMatcher matcher = new ContactMatcher();
487         ContentValues values = new ContentValues();
488 
489         aggregateContact(db, rawContactId, accountType, accountName, currentContactId, candidates,
490                 matcher, values);
491     }
492 
updateAggregateData(long contactId)493     public void updateAggregateData(long contactId) {
494         if (!mEnabled) {
495             return;
496         }
497 
498         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
499         computeAggregateData(db, contactId, mContactUpdate);
500         mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
501         mContactUpdate.execute();
502 
503         mDbHelper.updateContactVisible(contactId);
504         updateAggregatedPresence(contactId);
505     }
506 
updateAggregatedPresence(long contactId)507     private void updateAggregatedPresence(long contactId) {
508         mAggregatedPresenceReplace.bindLong(1, contactId);
509         mAggregatedPresenceReplace.bindLong(2, contactId);
510         mAggregatedPresenceReplace.execute();
511     }
512 
513     /**
514      * Given a specific raw contact, finds all matching aggregate contacts and chooses the one
515      * with the highest match score.  If no such contact is found, creates a new contact.
516      */
aggregateContact(SQLiteDatabase db, long rawContactId, String accountType, String accountName, long currentContactId, MatchCandidateList candidates, ContactMatcher matcher, ContentValues values)517     private synchronized void aggregateContact(SQLiteDatabase db, long rawContactId,
518             String accountType, String accountName, long currentContactId,
519             MatchCandidateList candidates, ContactMatcher matcher, ContentValues values) {
520 
521         int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
522 
523         Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId);
524         if (aggModeObject != null) {
525             aggregationMode = aggModeObject;
526         }
527 
528         long contactId = -1;
529         long contactIdToSplit = -1;
530 
531         if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
532             candidates.clear();
533             matcher.clear();
534 
535             contactId = pickBestMatchBasedOnExceptions(db, rawContactId, matcher);
536             if (contactId == -1) {
537                 contactId = pickBestMatchBasedOnData(db, rawContactId, candidates, matcher);
538 
539                 // If we found an aggregate to join, but it already contains raw contacts from
540                 // the same account, not only will we not join it, but also we will split
541                 // that other aggregate
542                 if (contactId != -1 && contactId != currentContactId &&
543                         containsRawContactsFromAccount(db, contactId, accountType, accountName)) {
544                     contactIdToSplit = contactId;
545                     contactId = -1;
546                 }
547             }
548         } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
549             return;
550         }
551 
552         long currentContactContentsCount = 0;
553 
554         if (currentContactId != 0) {
555             mRawContactCountQuery.bindLong(1, currentContactId);
556             mRawContactCountQuery.bindLong(2, rawContactId);
557             currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong();
558         }
559 
560         // If there are no other raw contacts in the current aggregate, we might as well reuse it.
561         // Also, if the aggregation mode is SUSPENDED, we must reuse the same aggregate.
562         if (contactId == -1
563                 && currentContactId != 0
564                 && (currentContactContentsCount == 0
565                         || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) {
566             contactId = currentContactId;
567         }
568 
569         if (contactId == currentContactId) {
570             // Aggregation unchanged
571             markAggregated(rawContactId);
572         } else if (contactId == -1) {
573             // Splitting an aggregate
574             createNewContactForRawContact(db, rawContactId);
575             if (currentContactContentsCount > 0) {
576                 updateAggregateData(currentContactId);
577             }
578         } else {
579             // Joining with an existing aggregate
580             if (currentContactContentsCount == 0) {
581                 // Delete a previous aggregate if it only contained this raw contact
582                 mContactDelete.bindLong(1, currentContactId);
583                 mContactDelete.execute();
584 
585                 mAggregatedPresenceDelete.bindLong(1, currentContactId);
586                 mAggregatedPresenceDelete.execute();
587             }
588 
589             setContactIdAndMarkAggregated(rawContactId, contactId);
590             computeAggregateData(db, contactId, mContactUpdate);
591             mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
592             mContactUpdate.execute();
593             mDbHelper.updateContactVisible(contactId);
594             updateAggregatedPresence(contactId);
595         }
596 
597         if (contactIdToSplit != -1) {
598             splitAutomaticallyAggregatedRawContacts(db, contactIdToSplit);
599         }
600     }
601 
602     /**
603      * Returns true if the aggregate contains has any raw contacts from the specified account.
604      */
containsRawContactsFromAccount( SQLiteDatabase db, long contactId, String accountType, String accountName)605     private boolean containsRawContactsFromAccount(
606             SQLiteDatabase db, long contactId, String accountType, String accountName) {
607         String query;
608         String[] args;
609         if (accountType == null) {
610             query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS +
611                     " WHERE " + RawContacts.CONTACT_ID + "=?" +
612                     " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL " +
613                     " AND " + RawContacts.ACCOUNT_NAME + " IS NULL ";
614             args = mSelectionArgs1;
615             args[0] = String.valueOf(contactId);
616         } else {
617             query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS +
618                     " WHERE " + RawContacts.CONTACT_ID + "=?" +
619                     " AND " + RawContacts.ACCOUNT_TYPE + "=?" +
620                     " AND " + RawContacts.ACCOUNT_NAME + "=?";
621             args = mSelectionArgs3;
622             args[0] = String.valueOf(contactId);
623             args[1] = accountType;
624             args[2] = accountName;
625         }
626         Cursor cursor = db.rawQuery(query, args);
627         try {
628             cursor.moveToFirst();
629             return cursor.getInt(0) != 0;
630         } finally {
631             cursor.close();
632         }
633     }
634 
635     /**
636      * Breaks up an existing aggregate when a new raw contact is inserted that has
637      * comes from the same account as one of the raw contacts in this aggregate.
638      */
splitAutomaticallyAggregatedRawContacts(SQLiteDatabase db, long contactId)639     private void splitAutomaticallyAggregatedRawContacts(SQLiteDatabase db, long contactId) {
640         mSelectionArgs1[0] = String.valueOf(contactId);
641         int count = (int) DatabaseUtils.longForQuery(db,
642                 "SELECT COUNT(" + RawContacts._ID + ")" +
643                 " FROM " + Tables.RAW_CONTACTS +
644                 " WHERE " + RawContacts.CONTACT_ID + "=?", mSelectionArgs1);
645         if (count < 2) {
646             // A single-raw-contact aggregate does not need to be split up
647             return;
648         }
649 
650         // Find all constituent raw contacts that are not held together by
651         // an explicit aggregation exception
652         String query =
653                 "SELECT " + RawContacts._ID +
654                 " FROM " + Tables.RAW_CONTACTS +
655                 " WHERE " + RawContacts.CONTACT_ID + "=?" +
656                 "   AND " + RawContacts._ID + " NOT IN " +
657                         "(SELECT " + AggregationExceptions.RAW_CONTACT_ID1 +
658                         " FROM " + Tables.AGGREGATION_EXCEPTIONS +
659                         " WHERE " + AggregationExceptions.TYPE + "="
660                                 + AggregationExceptions.TYPE_KEEP_TOGETHER +
661                         " UNION SELECT " + AggregationExceptions.RAW_CONTACT_ID2 +
662                         " FROM " + Tables.AGGREGATION_EXCEPTIONS +
663                         " WHERE " + AggregationExceptions.TYPE + "="
664                                 + AggregationExceptions.TYPE_KEEP_TOGETHER +
665                         ")";
666         Cursor cursor = db.rawQuery(query, mSelectionArgs1);
667         try {
668             // Process up to count-1 raw contact, leaving the last one alone.
669             for (int i = 0; i < count - 1; i++) {
670                 if (!cursor.moveToNext()) {
671                     break;
672                 }
673                 long rawContactId = cursor.getLong(0);
674                 createNewContactForRawContact(db, rawContactId);
675             }
676         } finally {
677             cursor.close();
678         }
679         if (contactId > 0) {
680             updateAggregateData(contactId);
681         }
682     }
683 
684     /**
685      * Creates a stand-alone Contact for the given raw contact ID.
686      */
createNewContactForRawContact(SQLiteDatabase db, long rawContactId)687     private void createNewContactForRawContact(SQLiteDatabase db, long rawContactId) {
688         mSelectionArgs1[0] = String.valueOf(rawContactId);
689         computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
690                 mContactInsert);
691         long contactId = mContactInsert.executeInsert();
692         setContactIdAndMarkAggregated(rawContactId, contactId);
693         mDbHelper.updateContactVisible(contactId);
694         setPresenceContactId(rawContactId, contactId);
695         updateAggregatedPresence(contactId);
696     }
697 
698     /**
699      * Updates the contact ID for the specified contact.
700      */
setContactId(long rawContactId, long contactId)701     private void setContactId(long rawContactId, long contactId) {
702         mContactIdUpdate.bindLong(1, contactId);
703         mContactIdUpdate.bindLong(2, rawContactId);
704         mContactIdUpdate.execute();
705     }
706 
707     /**
708      * Marks the specified raw contact ID as aggregated
709      */
markAggregated(long rawContactId)710     private void markAggregated(long rawContactId) {
711         mMarkAggregatedUpdate.bindLong(1, rawContactId);
712         mMarkAggregatedUpdate.execute();
713     }
714 
715     /**
716      * Updates the contact ID for the specified contact and marks the raw contact as aggregated.
717      */
setContactIdAndMarkAggregated(long rawContactId, long contactId)718     private void setContactIdAndMarkAggregated(long rawContactId, long contactId) {
719         mContactIdAndMarkAggregatedUpdate.bindLong(1, contactId);
720         mContactIdAndMarkAggregatedUpdate.bindLong(2, rawContactId);
721         mContactIdAndMarkAggregatedUpdate.execute();
722     }
723 
setPresenceContactId(long rawContactId, long contactId)724     private void setPresenceContactId(long rawContactId, long contactId) {
725         mPresenceContactIdUpdate.bindLong(1, contactId);
726         mPresenceContactIdUpdate.bindLong(2, rawContactId);
727         mPresenceContactIdUpdate.execute();
728     }
729 
730     interface AggregateExceptionPrefetchQuery {
731         String TABLE = Tables.AGGREGATION_EXCEPTIONS;
732 
733         String[] COLUMNS = {
734             AggregationExceptions.RAW_CONTACT_ID1,
735             AggregationExceptions.RAW_CONTACT_ID2,
736         };
737 
738         int RAW_CONTACT_ID1 = 0;
739         int RAW_CONTACT_ID2 = 1;
740     }
741 
742     // A set of raw contact IDs for which there are aggregation exceptions
743     private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>();
744     private boolean mAggregationExceptionIdsValid;
745 
invalidateAggregationExceptionCache()746     public void invalidateAggregationExceptionCache() {
747         mAggregationExceptionIdsValid = false;
748     }
749 
750     /**
751      * Finds all raw contact IDs for which there are aggregation exceptions. The list of
752      * ids is used as an optimization in aggregation: there is no point to run a query against
753      * the agg_exceptions table if it is known that there are no records there for a given
754      * raw contact ID.
755      */
prefetchAggregationExceptionIds(SQLiteDatabase db)756     private void prefetchAggregationExceptionIds(SQLiteDatabase db) {
757         mAggregationExceptionIds.clear();
758         final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE,
759                 AggregateExceptionPrefetchQuery.COLUMNS,
760                 null, null, null, null, null);
761 
762         try {
763             while (c.moveToNext()) {
764                 long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1);
765                 long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2);
766                 mAggregationExceptionIds.add(rawContactId1);
767                 mAggregationExceptionIds.add(rawContactId2);
768             }
769         } finally {
770             c.close();
771         }
772 
773         mAggregationExceptionIdsValid = true;
774     }
775 
776     interface AggregateExceptionQuery {
777         String TABLE = Tables.AGGREGATION_EXCEPTIONS
778             + " JOIN raw_contacts raw_contacts1 "
779                     + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) "
780             + " JOIN raw_contacts raw_contacts2 "
781                     + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) ";
782 
783         String[] COLUMNS = {
784             AggregationExceptions.TYPE,
785             AggregationExceptions.RAW_CONTACT_ID1,
786             "raw_contacts1." + RawContacts.CONTACT_ID,
787             "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED,
788             "raw_contacts2." + RawContacts.CONTACT_ID,
789             "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED,
790         };
791 
792         int TYPE = 0;
793         int RAW_CONTACT_ID1 = 1;
794         int CONTACT_ID1 = 2;
795         int AGGREGATION_NEEDED_1 = 3;
796         int CONTACT_ID2 = 4;
797         int AGGREGATION_NEEDED_2 = 5;
798     }
799 
800     /**
801      * Computes match scores based on exceptions entered by the user: always match and never match.
802      * Returns the aggregate contact with the always match exception if any.
803      */
pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId, ContactMatcher matcher)804     private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId,
805             ContactMatcher matcher) {
806         if (!mAggregationExceptionIdsValid) {
807             prefetchAggregationExceptionIds(db);
808         }
809 
810         // If there are no aggregation exceptions involving this raw contact, there is no need to
811         // run a query and we can just return -1, which stands for "nothing found"
812         if (!mAggregationExceptionIds.contains(rawContactId)) {
813             return -1;
814         }
815 
816         final Cursor c = db.query(AggregateExceptionQuery.TABLE,
817                 AggregateExceptionQuery.COLUMNS,
818                 AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId
819                         + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId,
820                 null, null, null, null);
821 
822         try {
823             while (c.moveToNext()) {
824                 int type = c.getInt(AggregateExceptionQuery.TYPE);
825                 long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1);
826                 long contactId = -1;
827                 if (rawContactId == rawContactId1) {
828                     if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_2) == 0
829                             && !c.isNull(AggregateExceptionQuery.CONTACT_ID2)) {
830                         contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2);
831                     }
832                 } else {
833                     if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_1) == 0
834                             && !c.isNull(AggregateExceptionQuery.CONTACT_ID1)) {
835                         contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1);
836                     }
837                 }
838                 if (contactId != -1) {
839                     if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) {
840                         matcher.keepIn(contactId);
841                     } else {
842                         matcher.keepOut(contactId);
843                     }
844                 }
845             }
846         } finally {
847             c.close();
848         }
849 
850         return matcher.pickBestMatch(ContactMatcher.MAX_SCORE, true);
851     }
852 
853     /**
854      * Picks the best matching contact based on matches between data elements.  It considers
855      * name match to be primary and phone, email etc matches to be secondary.  A good primary
856      * match triggers aggregation, while a good secondary match only triggers aggregation in
857      * the absence of a strong primary mismatch.
858      * <p>
859      * Consider these examples:
860      * <p>
861      * John Doe with phone number 111-111-1111 and Jon Doe with phone number 111-111-1111 should
862      * be aggregated (same number, similar names).
863      * <p>
864      * John Doe with phone number 111-111-1111 and Deborah Doe with phone number 111-111-1111 should
865      * not be aggregated (same number, different names).
866      */
pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, ContactMatcher matcher)867     private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId,
868             MatchCandidateList candidates, ContactMatcher matcher) {
869 
870         // Find good matches based on name alone
871         long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, candidates, matcher);
872         if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) {
873             // We found multiple matches on the name - do not aggregate because of the ambiguity
874             return -1;
875         } else if (bestMatch == -1) {
876             // We haven't found a good match on name, see if we have any matches on phone, email etc
877             bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher);
878             if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) {
879                 return -1;
880             }
881         }
882 
883         return bestMatch;
884     }
885 
886 
887     /**
888      * Picks the best matching contact based on secondary data matches.  The method loads
889      * structured names for all candidate contacts and recomputes match scores using approximate
890      * matching.
891      */
pickBestMatchBasedOnSecondaryData(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, ContactMatcher matcher)892     private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db,
893             long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
894         List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates(
895                 ContactMatcher.SCORE_THRESHOLD_PRIMARY);
896         if (secondaryContactIds == null || secondaryContactIds.size() > SECONDARY_HIT_LIMIT) {
897             return -1;
898         }
899 
900         loadNameMatchCandidates(db, rawContactId, candidates, true);
901 
902         mSb.setLength(0);
903         mSb.append(RawContacts.CONTACT_ID).append(" IN (");
904         for (int i = 0; i < secondaryContactIds.size(); i++) {
905             if (i != 0) {
906                 mSb.append(',');
907             }
908             mSb.append(secondaryContactIds.get(i));
909         }
910 
911         // We only want to compare structured names to structured names
912         // at this stage, we need to ignore all other sources of name lookup data.
913         mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL);
914 
915         matchAllCandidates(db, mSb.toString(), candidates, matcher,
916                 ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null);
917 
918         return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY, false);
919     }
920 
921     private interface NameLookupQuery {
922         String TABLE = Tables.NAME_LOOKUP;
923 
924         String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?";
925         String SELECTION_STRUCTURED_NAME_BASED =
926                 SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL;
927 
928         String[] COLUMNS = new String[] {
929                 NameLookupColumns.NORMALIZED_NAME,
930                 NameLookupColumns.NAME_TYPE
931         };
932 
933         int NORMALIZED_NAME = 0;
934         int NAME_TYPE = 1;
935     }
936 
loadNameMatchCandidates(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, boolean structuredNameBased)937     private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId,
938             MatchCandidateList candidates, boolean structuredNameBased) {
939         candidates.clear();
940         mSelectionArgs1[0] = String.valueOf(rawContactId);
941         Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
942                 structuredNameBased
943                         ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED
944                         : NameLookupQuery.SELECTION,
945                 mSelectionArgs1, null, null, null);
946         try {
947             while (c.moveToNext()) {
948                 String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME);
949                 int type = c.getInt(NameLookupQuery.NAME_TYPE);
950                 candidates.add(normalizedName, type);
951             }
952         } finally {
953             c.close();
954         }
955     }
956 
957     /**
958      * Computes scores for contacts that have matching data rows.
959      */
updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, ContactMatcher matcher)960     private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
961             MatchCandidateList candidates, ContactMatcher matcher) {
962 
963         updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
964         long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY, false);
965         if (bestMatch != -1) {
966             return bestMatch;
967         }
968 
969         updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
970         updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
971 
972         return -1;
973     }
974 
975     private interface NameLookupMatchQuery {
976         String TABLE = Tables.NAME_LOOKUP + " nameA"
977                 + " JOIN " + Tables.NAME_LOOKUP + " nameB" +
978                 " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "="
979                         + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")"
980                 + " JOIN " + Tables.RAW_CONTACTS +
981                 " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = "
982                         + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
983 
984         String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?"
985                 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
986 
987         String[] COLUMNS = new String[] {
988             RawContacts.CONTACT_ID,
989             "nameA." + NameLookupColumns.NORMALIZED_NAME,
990             "nameA." + NameLookupColumns.NAME_TYPE,
991             "nameB." + NameLookupColumns.NAME_TYPE,
992         };
993 
994         int CONTACT_ID = 0;
995         int NAME = 1;
996         int NAME_TYPE_A = 2;
997         int NAME_TYPE_B = 3;
998     }
999 
updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId, ContactMatcher matcher)1000     private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
1001             ContactMatcher matcher) {
1002         mSelectionArgs1[0] = String.valueOf(rawContactId);
1003         Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS,
1004                 NameLookupMatchQuery.SELECTION,
1005                 mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING);
1006         try {
1007             while (c.moveToNext()) {
1008                 long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID);
1009                 String name = c.getString(NameLookupMatchQuery.NAME);
1010                 int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A);
1011                 int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B);
1012                 matcher.matchName(contactId, nameTypeA, name,
1013                         nameTypeB, name, ContactMatcher.MATCHING_ALGORITHM_EXACT);
1014                 if (nameTypeA == NameLookupType.NICKNAME &&
1015                         nameTypeB == NameLookupType.NICKNAME) {
1016                     matcher.updateScoreWithNicknameMatch(contactId);
1017                 }
1018             }
1019         } finally {
1020             c.close();
1021         }
1022     }
1023 
1024     private interface EmailLookupQuery {
1025         String TABLE = Tables.DATA + " dataA"
1026                 + " JOIN " + Tables.DATA + " dataB" +
1027                 " ON (" + "dataA." + Email.DATA + "=dataB." + Email.DATA + ")"
1028                 + " JOIN " + Tables.RAW_CONTACTS +
1029                 " ON (dataB." + Data.RAW_CONTACT_ID + " = "
1030                         + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1031 
1032         String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
1033                 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?"
1034                 + " AND dataA." + Email.DATA + " NOT NULL"
1035                 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?"
1036                 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
1037 
1038         String[] COLUMNS = new String[] {
1039             RawContacts.CONTACT_ID
1040         };
1041 
1042         int CONTACT_ID = 0;
1043     }
1044 
updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId, ContactMatcher matcher)1045     private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
1046             ContactMatcher matcher) {
1047         mSelectionArgs3[0] = String.valueOf(rawContactId);
1048         mSelectionArgs3[1] = mSelectionArgs3[2] = String.valueOf(mMimeTypeIdEmail);
1049         Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
1050                 EmailLookupQuery.SELECTION,
1051                 mSelectionArgs3, null, null, null, SECONDARY_HIT_LIMIT_STRING);
1052         try {
1053             while (c.moveToNext()) {
1054                 long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
1055                 matcher.updateScoreWithEmailMatch(contactId);
1056             }
1057         } finally {
1058             c.close();
1059         }
1060     }
1061 
1062     private interface PhoneLookupQuery {
1063         String TABLE = Tables.PHONE_LOOKUP + " phoneA"
1064                 + " JOIN " + Tables.DATA + " dataA"
1065                 + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
1066                 + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
1067                 + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
1068                         + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
1069                 + " JOIN " + Tables.DATA + " dataB"
1070                 + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
1071                 + " JOIN " + Tables.RAW_CONTACTS
1072                 + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
1073                         + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1074 
1075         String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
1076                 + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
1077                         + "dataB." + Phone.NUMBER + ",?)"
1078                 + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
1079 
1080         String[] COLUMNS = new String[] {
1081             RawContacts.CONTACT_ID
1082         };
1083 
1084         int CONTACT_ID = 0;
1085     }
1086 
updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId, ContactMatcher matcher)1087     private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
1088             ContactMatcher matcher) {
1089         mSelectionArgs2[0] = String.valueOf(rawContactId);
1090         mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter();
1091         Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
1092                 PhoneLookupQuery.SELECTION,
1093                 mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
1094         try {
1095             while (c.moveToNext()) {
1096                 long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
1097                 matcher.updateScoreWithPhoneNumberMatch(contactId);
1098             }
1099         } finally {
1100             c.close();
1101         }
1102 
1103     }
1104 
1105     /**
1106      * Loads name lookup rows for approximate name matching and updates match scores based on that
1107      * data.
1108      */
lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates, ContactMatcher matcher)1109     private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
1110             ContactMatcher matcher) {
1111         HashSet<String> firstLetters = new HashSet<String>();
1112         for (int i = 0; i < candidates.mCount; i++) {
1113             final NameMatchCandidate candidate = candidates.mList.get(i);
1114             if (candidate.mName.length() >= 2) {
1115                 String firstLetter = candidate.mName.substring(0, 2);
1116                 if (!firstLetters.contains(firstLetter)) {
1117                     firstLetters.add(firstLetter);
1118                     final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '"
1119                             + firstLetter + "*') AND "
1120                             + NameLookupColumns.NAME_TYPE + " IN("
1121                                     + NameLookupType.NAME_COLLATION_KEY + ","
1122                                     + NameLookupType.EMAIL_BASED_NICKNAME + ","
1123                                     + NameLookupType.NICKNAME + ")";
1124                     matchAllCandidates(db, selection, candidates, matcher,
1125                             ContactMatcher.MATCHING_ALGORITHM_APPROXIMATE,
1126                             String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT));
1127                 }
1128             }
1129         }
1130     }
1131 
1132     private interface ContactNameLookupQuery {
1133         String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
1134 
1135         String[] COLUMNS = new String[] {
1136                 RawContacts.CONTACT_ID,
1137                 NameLookupColumns.NORMALIZED_NAME,
1138                 NameLookupColumns.NAME_TYPE
1139         };
1140 
1141         int CONTACT_ID = 0;
1142         int NORMALIZED_NAME = 1;
1143         int NAME_TYPE = 2;
1144     }
1145 
1146     /**
1147      * Loads all candidate rows from the name lookup table and updates match scores based
1148      * on that data.
1149      */
matchAllCandidates(SQLiteDatabase db, String selection, MatchCandidateList candidates, ContactMatcher matcher, int algorithm, String limit)1150     private void matchAllCandidates(SQLiteDatabase db, String selection,
1151             MatchCandidateList candidates, ContactMatcher matcher, int algorithm, String limit) {
1152         final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
1153                 selection, null, null, null, null, limit);
1154 
1155         try {
1156             while (c.moveToNext()) {
1157                 Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
1158                 String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
1159                 int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
1160 
1161                 // Note the N^2 complexity of the following fragment. This is not a huge concern
1162                 // since the number of candidates is very small and in general secondary hits
1163                 // in the absence of primary hits are rare.
1164                 for (int i = 0; i < candidates.mCount; i++) {
1165                     NameMatchCandidate candidate = candidates.mList.get(i);
1166                     matcher.matchName(contactId, candidate.mLookupType, candidate.mName,
1167                             nameType, name, algorithm);
1168                 }
1169             }
1170         } finally {
1171             c.close();
1172         }
1173     }
1174 
1175     private interface RawContactsQuery {
1176         String SQL_FORMAT =
1177                 "SELECT "
1178                         + RawContactsColumns.CONCRETE_ID + ","
1179                         + RawContactsColumns.DISPLAY_NAME + ","
1180                         + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
1181                         + RawContacts.ACCOUNT_TYPE + ","
1182                         + RawContacts.ACCOUNT_NAME + ","
1183                         + RawContacts.SOURCE_ID + ","
1184                         + RawContacts.CUSTOM_RINGTONE + ","
1185                         + RawContacts.SEND_TO_VOICEMAIL + ","
1186                         + RawContacts.LAST_TIME_CONTACTED + ","
1187                         + RawContacts.TIMES_CONTACTED + ","
1188                         + RawContacts.STARRED + ","
1189                         + RawContacts.IS_RESTRICTED + ","
1190                         + RawContacts.NAME_VERIFIED + ","
1191                         + DataColumns.CONCRETE_ID + ","
1192                         + DataColumns.CONCRETE_MIMETYPE_ID + ","
1193                         + Data.IS_SUPER_PRIMARY +
1194                 " FROM " + Tables.RAW_CONTACTS +
1195                 " LEFT OUTER JOIN " + Tables.DATA +
1196                 " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
1197                         + " AND ((" + DataColumns.MIMETYPE_ID + "=%d"
1198                                 + " AND " + Photo.PHOTO + " NOT NULL)"
1199                         + " OR (" + DataColumns.MIMETYPE_ID + "=%d"
1200                                 + " AND " + Phone.NUMBER + " NOT NULL)))";
1201 
1202         String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT +
1203                 " WHERE " + RawContactsColumns.CONCRETE_ID + "=?";
1204 
1205         String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT +
1206                 " WHERE " + RawContacts.CONTACT_ID + "=?"
1207                 + " AND " + RawContacts.DELETED + "=0";
1208 
1209         int RAW_CONTACT_ID = 0;
1210         int DISPLAY_NAME = 1;
1211         int DISPLAY_NAME_SOURCE = 2;
1212         int ACCOUNT_TYPE = 3;
1213         int ACCOUNT_NAME = 4;
1214         int SOURCE_ID = 5;
1215         int CUSTOM_RINGTONE = 6;
1216         int SEND_TO_VOICEMAIL = 7;
1217         int LAST_TIME_CONTACTED = 8;
1218         int TIMES_CONTACTED = 9;
1219         int STARRED = 10;
1220         int IS_RESTRICTED = 11;
1221         int NAME_VERIFIED = 12;
1222         int DATA_ID = 13;
1223         int MIMETYPE_ID = 14;
1224         int IS_SUPER_PRIMARY = 15;
1225     }
1226 
1227     private interface ContactReplaceSqlStatement {
1228         String UPDATE_SQL =
1229                 "UPDATE " + Tables.CONTACTS +
1230                 " SET "
1231                         + Contacts.NAME_RAW_CONTACT_ID + "=?, "
1232                         + Contacts.PHOTO_ID + "=?, "
1233                         + Contacts.SEND_TO_VOICEMAIL + "=?, "
1234                         + Contacts.CUSTOM_RINGTONE + "=?, "
1235                         + Contacts.LAST_TIME_CONTACTED + "=?, "
1236                         + Contacts.TIMES_CONTACTED + "=?, "
1237                         + Contacts.STARRED + "=?, "
1238                         + Contacts.HAS_PHONE_NUMBER + "=?, "
1239                         + ContactsColumns.SINGLE_IS_RESTRICTED + "=?, "
1240                         + Contacts.LOOKUP_KEY + "=? " +
1241                 " WHERE " + Contacts._ID + "=?";
1242 
1243         String INSERT_SQL =
1244                 "INSERT INTO " + Tables.CONTACTS + " ("
1245                         + Contacts.NAME_RAW_CONTACT_ID + ", "
1246                         + Contacts.PHOTO_ID + ", "
1247                         + Contacts.SEND_TO_VOICEMAIL + ", "
1248                         + Contacts.CUSTOM_RINGTONE + ", "
1249                         + Contacts.LAST_TIME_CONTACTED + ", "
1250                         + Contacts.TIMES_CONTACTED + ", "
1251                         + Contacts.STARRED + ", "
1252                         + Contacts.HAS_PHONE_NUMBER + ", "
1253                         + ContactsColumns.SINGLE_IS_RESTRICTED + ", "
1254                         + Contacts.LOOKUP_KEY + ", "
1255                         + Contacts.IN_VISIBLE_GROUP + ") " +
1256                 " VALUES (?,?,?,?,?,?,?,?,?,?,0)";
1257 
1258         int NAME_RAW_CONTACT_ID = 1;
1259         int PHOTO_ID = 2;
1260         int SEND_TO_VOICEMAIL = 3;
1261         int CUSTOM_RINGTONE = 4;
1262         int LAST_TIME_CONTACTED = 5;
1263         int TIMES_CONTACTED = 6;
1264         int STARRED = 7;
1265         int HAS_PHONE_NUMBER = 8;
1266         int SINGLE_IS_RESTRICTED = 9;
1267         int LOOKUP_KEY = 10;
1268         int CONTACT_ID = 11;
1269     }
1270 
1271     /**
1272      * Computes aggregate-level data for the specified aggregate contact ID.
1273      */
computeAggregateData(SQLiteDatabase db, long contactId, SQLiteStatement statement)1274     private void computeAggregateData(SQLiteDatabase db, long contactId,
1275             SQLiteStatement statement) {
1276         mSelectionArgs1[0] = String.valueOf(contactId);
1277         computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement);
1278     }
1279 
1280     /**
1281      * Computes aggregate-level data from constituent raw contacts.
1282      */
computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs, SQLiteStatement statement)1283     private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs,
1284             SQLiteStatement statement) {
1285         long currentRawContactId = -1;
1286         long bestPhotoId = -1;
1287         boolean foundSuperPrimaryPhoto = false;
1288         int photoPriority = -1;
1289         int totalRowCount = 0;
1290         int contactSendToVoicemail = 0;
1291         String contactCustomRingtone = null;
1292         long contactLastTimeContacted = 0;
1293         int contactTimesContacted = 0;
1294         int contactStarred = 0;
1295         int singleIsRestricted = 1;
1296         int hasPhoneNumber = 0;
1297 
1298         mDisplayNameCandidate.clear();
1299 
1300         mSb.setLength(0);       // Lookup key
1301         Cursor c = db.rawQuery(sql, sqlArgs);
1302         try {
1303             while (c.moveToNext()) {
1304                 long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
1305                 if (rawContactId != currentRawContactId) {
1306                     currentRawContactId = rawContactId;
1307                     totalRowCount++;
1308 
1309                     // Display name
1310                     String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
1311                     int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE);
1312                     int nameVerified = c.getInt(RawContactsQuery.NAME_VERIFIED);
1313                     String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
1314                     processDisplayNameCanditate(rawContactId, displayName, displayNameSource,
1315                             mContactsProvider.isWritableAccount(accountType), nameVerified != 0);
1316 
1317 
1318                     // Contact options
1319                     if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
1320                         boolean sendToVoicemail =
1321                                 (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0);
1322                         if (sendToVoicemail) {
1323                             contactSendToVoicemail++;
1324                         }
1325                     }
1326 
1327                     if (contactCustomRingtone == null
1328                             && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) {
1329                         contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE);
1330                     }
1331 
1332                     long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED);
1333                     if (lastTimeContacted > contactLastTimeContacted) {
1334                         contactLastTimeContacted = lastTimeContacted;
1335                     }
1336 
1337                     int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED);
1338                     if (timesContacted > contactTimesContacted) {
1339                         contactTimesContacted = timesContacted;
1340                     }
1341 
1342                     if (c.getInt(RawContactsQuery.STARRED) != 0) {
1343                         contactStarred = 1;
1344                     }
1345 
1346                     // Single restricted
1347                     if (totalRowCount > 1) {
1348                         // Not single
1349                         singleIsRestricted = 0;
1350                     } else {
1351                         int isRestricted = c.getInt(RawContactsQuery.IS_RESTRICTED);
1352 
1353                         if (isRestricted == 0) {
1354                             // Not restricted
1355                             singleIsRestricted = 0;
1356                         }
1357                     }
1358 
1359                     ContactLookupKey.appendToLookupKey(mSb,
1360                             c.getString(RawContactsQuery.ACCOUNT_TYPE),
1361                             c.getString(RawContactsQuery.ACCOUNT_NAME),
1362                             rawContactId,
1363                             c.getString(RawContactsQuery.SOURCE_ID),
1364                             displayName);
1365                 }
1366 
1367                 if (!c.isNull(RawContactsQuery.DATA_ID)) {
1368                     long dataId = c.getLong(RawContactsQuery.DATA_ID);
1369                     int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
1370                     boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0;
1371                     if (mimetypeId == mMimeTypeIdPhoto) {
1372                         if (!foundSuperPrimaryPhoto) {
1373                             String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
1374                             int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
1375                             if (superPrimary || priority > photoPriority) {
1376                                 photoPriority = priority;
1377                                 bestPhotoId = dataId;
1378                                 foundSuperPrimaryPhoto |= superPrimary;
1379                             }
1380                         }
1381                     } else if (mimetypeId == mMimeTypeIdPhone) {
1382                         hasPhoneNumber = 1;
1383                     }
1384                 }
1385             }
1386         } finally {
1387             c.close();
1388         }
1389 
1390         statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID,
1391                 mDisplayNameCandidate.rawContactId);
1392 
1393         if (bestPhotoId != -1) {
1394             statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId);
1395         } else {
1396             statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID);
1397         }
1398 
1399         statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL,
1400                 totalRowCount == contactSendToVoicemail ? 1 : 0);
1401         DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
1402                 contactCustomRingtone);
1403         statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED,
1404                 contactLastTimeContacted);
1405         statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED,
1406                 contactTimesContacted);
1407         statement.bindLong(ContactReplaceSqlStatement.STARRED,
1408                 contactStarred);
1409         statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER,
1410                 hasPhoneNumber);
1411         statement.bindLong(ContactReplaceSqlStatement.SINGLE_IS_RESTRICTED,
1412                 singleIsRestricted);
1413         statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
1414                 Uri.encode(mSb.toString()));
1415     }
1416 
1417     /**
1418      * Uses the supplied values to determine if they represent a "better" display name
1419      * for the aggregate contact currently evaluated.  If so, it updates
1420      * {@link #mDisplayNameCandidate} with the new values.
1421      */
processDisplayNameCanditate(long rawContactId, String displayName, int displayNameSource, boolean writableAccount, boolean verified)1422     private void processDisplayNameCanditate(long rawContactId, String displayName,
1423             int displayNameSource, boolean writableAccount, boolean verified) {
1424 
1425         boolean replace = false;
1426         if (mDisplayNameCandidate.rawContactId == -1) {
1427             // No previous values available
1428             replace = true;
1429         } else if (!TextUtils.isEmpty(displayName)) {
1430             if (!mDisplayNameCandidate.verified && verified) {
1431                 // A verified name is better than any other name
1432                 replace = true;
1433             } else if (mDisplayNameCandidate.verified == verified) {
1434                 if (mDisplayNameCandidate.displayNameSource < displayNameSource) {
1435                     // New values come from an superior source, e.g. structured name vs phone number
1436                     replace = true;
1437                 } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) {
1438                     if (!mDisplayNameCandidate.writableAccount && writableAccount) {
1439                         replace = true;
1440                     } else if (mDisplayNameCandidate.writableAccount == writableAccount) {
1441                         if (NameNormalizer.compareComplexity(displayName,
1442                                 mDisplayNameCandidate.displayName) > 0) {
1443                             // New name is more complex than the previously found one
1444                             replace = true;
1445                         }
1446                     }
1447                 }
1448             }
1449         }
1450 
1451         if (replace) {
1452             mDisplayNameCandidate.rawContactId = rawContactId;
1453             mDisplayNameCandidate.displayName = displayName;
1454             mDisplayNameCandidate.displayNameSource = displayNameSource;
1455             mDisplayNameCandidate.verified = verified;
1456             mDisplayNameCandidate.writableAccount = writableAccount;
1457         }
1458     }
1459 
1460     private interface PhotoIdQuery {
1461         String[] COLUMNS = new String[] {
1462             RawContacts.ACCOUNT_TYPE,
1463             DataColumns.CONCRETE_ID,
1464             Data.IS_SUPER_PRIMARY,
1465         };
1466 
1467         int ACCOUNT_TYPE = 0;
1468         int DATA_ID = 1;
1469         int IS_SUPER_PRIMARY = 2;
1470     }
1471 
updatePhotoId(SQLiteDatabase db, long rawContactId)1472     public void updatePhotoId(SQLiteDatabase db, long rawContactId) {
1473 
1474         long contactId = mDbHelper.getContactId(rawContactId);
1475         if (contactId == 0) {
1476             return;
1477         }
1478 
1479         long bestPhotoId = -1;
1480         int photoPriority = -1;
1481 
1482         long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
1483 
1484         String tables = Tables.RAW_CONTACTS + " JOIN " + Tables.DATA + " ON("
1485                 + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
1486                 + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
1487                         + Photo.PHOTO + " NOT NULL))";
1488 
1489         mSelectionArgs1[0] = String.valueOf(contactId);
1490         final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS,
1491                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
1492         try {
1493             while (c.moveToNext()) {
1494                 long dataId = c.getLong(PhotoIdQuery.DATA_ID);
1495                 boolean superprimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0;
1496                 if (superprimary) {
1497                     bestPhotoId = dataId;
1498                     break;
1499                 }
1500 
1501                 String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE);
1502                 int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
1503                 if (priority > photoPriority) {
1504                     photoPriority = priority;
1505                     bestPhotoId = dataId;
1506                 }
1507             }
1508         } finally {
1509             c.close();
1510         }
1511 
1512         if (bestPhotoId == -1) {
1513             mPhotoIdUpdate.bindNull(1);
1514         } else {
1515             mPhotoIdUpdate.bindLong(1, bestPhotoId);
1516         }
1517         mPhotoIdUpdate.bindLong(2, contactId);
1518         mPhotoIdUpdate.execute();
1519     }
1520 
1521     private interface DisplayNameQuery {
1522         String[] COLUMNS = new String[] {
1523             RawContacts._ID,
1524             RawContactsColumns.DISPLAY_NAME,
1525             RawContactsColumns.DISPLAY_NAME_SOURCE,
1526             RawContacts.NAME_VERIFIED,
1527             RawContacts.SOURCE_ID,
1528             RawContacts.ACCOUNT_TYPE,
1529         };
1530 
1531         int _ID = 0;
1532         int DISPLAY_NAME = 1;
1533         int DISPLAY_NAME_SOURCE = 2;
1534         int NAME_VERIFIED = 3;
1535         int SOURCE_ID = 4;
1536         int ACCOUNT_TYPE = 5;
1537     }
1538 
updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId)1539     public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) {
1540         long contactId = mDbHelper.getContactId(rawContactId);
1541         if (contactId == 0) {
1542             return;
1543         }
1544 
1545         updateDisplayNameForContact(db, contactId);
1546     }
1547 
updateDisplayNameForContact(SQLiteDatabase db, long contactId)1548     public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) {
1549         boolean lookupKeyUpdateNeeded = false;
1550 
1551         mDisplayNameCandidate.clear();
1552 
1553         mSelectionArgs1[0] = String.valueOf(contactId);
1554         final Cursor c = db.query(Tables.RAW_CONTACTS, DisplayNameQuery.COLUMNS,
1555                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
1556         try {
1557             while (c.moveToNext()) {
1558                 long rawContactId = c.getLong(DisplayNameQuery._ID);
1559                 String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME);
1560                 int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE);
1561                 int nameVerified = c.getInt(DisplayNameQuery.NAME_VERIFIED);
1562                 String accountType = c.getString(DisplayNameQuery.ACCOUNT_TYPE);
1563 
1564                 processDisplayNameCanditate(rawContactId, displayName, displayNameSource,
1565                         mContactsProvider.isWritableAccount(accountType), nameVerified != 0);
1566 
1567                 // If the raw contact has no source id, the lookup key is based on the display
1568                 // name, so the lookup key needs to be updated.
1569                 lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID);
1570             }
1571         } finally {
1572             c.close();
1573         }
1574 
1575         if (mDisplayNameCandidate.rawContactId != -1) {
1576             mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId);
1577             mDisplayNameUpdate.bindLong(2, contactId);
1578             mDisplayNameUpdate.execute();
1579         }
1580 
1581         if (lookupKeyUpdateNeeded) {
1582             updateLookupKeyForContact(db, contactId);
1583         }
1584     }
1585 
1586     /**
1587      * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the
1588      * specified raw contact.
1589      */
updateHasPhoneNumber(SQLiteDatabase db, long rawContactId)1590     public void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) {
1591 
1592         long contactId = mDbHelper.getContactId(rawContactId);
1593         if (contactId == 0) {
1594             return;
1595         }
1596 
1597         mHasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE));
1598         mHasPhoneNumberUpdate.bindLong(2, contactId);
1599         mHasPhoneNumberUpdate.bindLong(3, contactId);
1600         mHasPhoneNumberUpdate.execute();
1601     }
1602 
1603     private interface LookupKeyQuery {
1604         String[] COLUMNS = new String[] {
1605             RawContacts._ID,
1606             RawContactsColumns.DISPLAY_NAME,
1607             RawContacts.ACCOUNT_TYPE,
1608             RawContacts.ACCOUNT_NAME,
1609             RawContacts.SOURCE_ID,
1610         };
1611 
1612         int ID = 0;
1613         int DISPLAY_NAME = 1;
1614         int ACCOUNT_TYPE = 2;
1615         int ACCOUNT_NAME = 3;
1616         int SOURCE_ID = 4;
1617     }
1618 
updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId)1619     public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
1620         long contactId = mDbHelper.getContactId(rawContactId);
1621         if (contactId == 0) {
1622             return;
1623         }
1624 
1625         updateLookupKeyForContact(db, contactId);
1626     }
1627 
updateLookupKeyForContact(SQLiteDatabase db, long contactId)1628     public void updateLookupKeyForContact(SQLiteDatabase db, long contactId) {
1629         mSb.setLength(0);
1630         mSelectionArgs1[0] = String.valueOf(contactId);
1631         final Cursor c = db.query(Tables.RAW_CONTACTS, LookupKeyQuery.COLUMNS,
1632                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID);
1633         try {
1634             while (c.moveToNext()) {
1635                 ContactLookupKey.appendToLookupKey(mSb,
1636                         c.getString(LookupKeyQuery.ACCOUNT_TYPE),
1637                         c.getString(LookupKeyQuery.ACCOUNT_NAME),
1638                         c.getLong(LookupKeyQuery.ID),
1639                         c.getString(LookupKeyQuery.SOURCE_ID),
1640                         c.getString(LookupKeyQuery.DISPLAY_NAME));
1641             }
1642         } finally {
1643             c.close();
1644         }
1645 
1646         if (mSb.length() == 0) {
1647             mLookupKeyUpdate.bindNull(1);
1648         } else {
1649             mLookupKeyUpdate.bindString(1, Uri.encode(mSb.toString()));
1650         }
1651         mLookupKeyUpdate.bindLong(2, contactId);
1652 
1653         mLookupKeyUpdate.execute();
1654     }
1655 
1656     /**
1657      * Execute {@link SQLiteStatement} that will update the
1658      * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}.
1659      */
updateStarred(long rawContactId)1660     protected void updateStarred(long rawContactId) {
1661         long contactId = mDbHelper.getContactId(rawContactId);
1662         if (contactId == 0) {
1663             return;
1664         }
1665 
1666         mStarredUpdate.bindLong(1, contactId);
1667         mStarredUpdate.execute();
1668     }
1669 
1670     /**
1671      * Finds matching contacts and returns a cursor on those.
1672      */
queryAggregationSuggestions(SQLiteQueryBuilder qb, String[] projection, long contactId, int maxSuggestions, String filter)1673     public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb, String[] projection,
1674             long contactId, int maxSuggestions, String filter) {
1675         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
1676 
1677         List<MatchScore> bestMatches = findMatchingContacts(db, contactId);
1678         return queryMatchingContacts(qb, db, contactId, projection, bestMatches, maxSuggestions,
1679                 filter);
1680     }
1681 
1682     private interface ContactIdQuery {
1683         String[] COLUMNS = new String[] {
1684             Contacts._ID
1685         };
1686 
1687         int _ID = 0;
1688     }
1689 
1690     /**
1691      * Loads contacts with specified IDs and returns them in the order of IDs in the
1692      * supplied list.
1693      */
queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db, long contactId, String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter)1694     private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db, long contactId,
1695             String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) {
1696 
1697         StringBuilder sb = new StringBuilder();
1698         sb.append(Contacts._ID);
1699         sb.append(" IN (");
1700         for (int i = 0; i < bestMatches.size(); i++) {
1701             MatchScore matchScore = bestMatches.get(i);
1702             if (i != 0) {
1703                 sb.append(",");
1704             }
1705             sb.append(matchScore.getContactId());
1706         }
1707         sb.append(")");
1708 
1709         if (!TextUtils.isEmpty(filter)) {
1710             sb.append(" AND " + Contacts._ID + " IN ");
1711             mContactsProvider.appendContactFilterAsNestedQuery(sb, filter);
1712         }
1713 
1714         // Run a query and find ids of best matching contacts satisfying the filter (if any)
1715         HashSet<Long> foundIds = new HashSet<Long>();
1716         Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(),
1717                 null, null, null, null);
1718         try {
1719             while(cursor.moveToNext()) {
1720                 foundIds.add(cursor.getLong(ContactIdQuery._ID));
1721             }
1722         } finally {
1723             cursor.close();
1724         }
1725 
1726         // Exclude all contacts that did not match the filter
1727         Iterator<MatchScore> iter = bestMatches.iterator();
1728         while (iter.hasNext()) {
1729             long id = iter.next().getContactId();
1730             if (!foundIds.contains(id)) {
1731                 iter.remove();
1732             }
1733         }
1734 
1735         // Limit the number of returned suggestions
1736         if (bestMatches.size() > maxSuggestions) {
1737             bestMatches = bestMatches.subList(0, maxSuggestions);
1738         }
1739 
1740         // Build an in-clause with the remaining contact IDs
1741         sb.setLength(0);
1742         sb.append(Contacts._ID);
1743         sb.append(" IN (");
1744         for (int i = 0; i < bestMatches.size(); i++) {
1745             MatchScore matchScore = bestMatches.get(i);
1746             if (i != 0) {
1747                 sb.append(",");
1748             }
1749             sb.append(matchScore.getContactId());
1750         }
1751         sb.append(")");
1752 
1753         // Run the final query with the required projection and contact IDs found by the first query
1754         cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID);
1755 
1756         // Build a sorted list of discovered IDs
1757         ArrayList<Long> sortedContactIds = new ArrayList<Long>(bestMatches.size());
1758         for (MatchScore matchScore : bestMatches) {
1759             sortedContactIds.add(matchScore.getContactId());
1760         }
1761 
1762         Collections.sort(sortedContactIds);
1763 
1764         // Map cursor indexes according to the descending order of match scores
1765         int[] positionMap = new int[bestMatches.size()];
1766         for (int i = 0; i < positionMap.length; i++) {
1767             long id = bestMatches.get(i).getContactId();
1768             positionMap[i] = sortedContactIds.indexOf(id);
1769         }
1770 
1771         return new ReorderingCursorWrapper(cursor, positionMap);
1772     }
1773 
1774     private interface RawContactIdQuery {
1775         String TABLE = Tables.RAW_CONTACTS;
1776 
1777         String[] COLUMNS = new String[] {
1778             RawContacts._ID
1779         };
1780 
1781         int _ID = 0;
1782     }
1783 
1784     /**
1785      * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
1786      * descending order of match score.
1787      */
findMatchingContacts(final SQLiteDatabase db, long contactId)1788     private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId) {
1789 
1790         MatchCandidateList candidates = new MatchCandidateList();
1791         ContactMatcher matcher = new ContactMatcher();
1792 
1793         // Don't aggregate a contact with itself
1794         matcher.keepOut(contactId);
1795 
1796         final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
1797                 RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
1798         try {
1799             while (c.moveToNext()) {
1800                 long rawContactId = c.getLong(RawContactIdQuery._ID);
1801                 updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
1802                         matcher);
1803             }
1804         } finally {
1805             c.close();
1806         }
1807 
1808         return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST);
1809     }
1810 
1811     /**
1812      * Computes scores for contacts that have matching data rows.
1813      */
updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, ContactMatcher matcher)1814     private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
1815             long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
1816 
1817         updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
1818         updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
1819         updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
1820         loadNameMatchCandidates(db, rawContactId, candidates, false);
1821         lookupApproximateNameMatches(db, candidates, matcher);
1822     }
1823 }
1824