ContactAggregator.java revision 098215569d35a130c883933ec76773e85356387e
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
17package com.android.providers.contacts.aggregation;
18
19import android.database.Cursor;
20import android.database.DatabaseUtils;
21import android.database.sqlite.SQLiteDatabase;
22import android.database.sqlite.SQLiteQueryBuilder;
23import android.database.sqlite.SQLiteStatement;
24import android.net.Uri;
25import android.provider.ContactsContract.AggregationExceptions;
26import android.provider.ContactsContract.CommonDataKinds.Email;
27import android.provider.ContactsContract.CommonDataKinds.Identity;
28import android.provider.ContactsContract.CommonDataKinds.Phone;
29import android.provider.ContactsContract.CommonDataKinds.Photo;
30import android.provider.ContactsContract.Contacts;
31import android.provider.ContactsContract.Contacts.AggregationSuggestions;
32import android.provider.ContactsContract.Data;
33import android.provider.ContactsContract.DisplayNameSources;
34import android.provider.ContactsContract.FullNameStyle;
35import android.provider.ContactsContract.PhotoFiles;
36import android.provider.ContactsContract.PinnedPositions;
37import android.provider.ContactsContract.RawContacts;
38import android.provider.ContactsContract.StatusUpdates;
39import android.text.TextUtils;
40import android.util.EventLog;
41import android.util.Log;
42
43import com.android.internal.annotations.VisibleForTesting;
44import com.android.providers.contacts.ContactLookupKey;
45import com.android.providers.contacts.ContactsDatabaseHelper;
46import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
47import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
48import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
49import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
50import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
51import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
52import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
53import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
54import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
55import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
56import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
57import com.android.providers.contacts.ContactsDatabaseHelper.Views;
58import com.android.providers.contacts.ContactsProvider2;
59import com.android.providers.contacts.NameLookupBuilder;
60import com.android.providers.contacts.NameNormalizer;
61import com.android.providers.contacts.NameSplitter;
62import com.android.providers.contacts.PhotoPriorityResolver;
63import com.android.providers.contacts.ReorderingCursorWrapper;
64import com.android.providers.contacts.TransactionContext;
65import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
66import com.android.providers.contacts.aggregation.util.ContactMatcher;
67import com.android.providers.contacts.aggregation.util.ContactMatcher.MatchScore;
68import com.android.providers.contacts.database.ContactsTableUtil;
69import com.android.providers.contacts.util.Clock;
70
71import com.google.android.collect.Maps;
72import com.google.android.collect.Sets;
73import com.google.common.collect.Multimap;
74import com.google.common.collect.HashMultimap;
75
76import java.util.ArrayList;
77import java.util.Collections;
78import java.util.HashMap;
79import java.util.HashSet;
80import java.util.Iterator;
81import java.util.List;
82import java.util.Locale;
83import java.util.Set;
84
85/**
86 * ContactAggregator deals with aggregating contact information coming from different sources.
87 * Two John Doe contacts from two disjoint sources are presumed to be the same
88 * person unless the user declares otherwise.
89 */
90public class ContactAggregator {
91
92    private static final String TAG = "ContactAggregator";
93
94    private static final boolean DEBUG_LOGGING = Log.isLoggable(TAG, Log.DEBUG);
95    private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
96
97    private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL =
98            NameLookupColumns.NAME_TYPE + " IN ("
99                    + NameLookupType.NAME_EXACT + ","
100                    + NameLookupType.NAME_VARIANT + ","
101                    + NameLookupType.NAME_COLLATION_KEY + ")";
102
103
104    /**
105     * SQL statement that sets the {@link ContactsColumns#LAST_STATUS_UPDATE_ID} column
106     * on the contact to point to the latest social status update.
107     */
108    private static final String UPDATE_LAST_STATUS_UPDATE_ID_SQL =
109            "UPDATE " + Tables.CONTACTS +
110            " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
111                    "(SELECT " + DataColumns.CONCRETE_ID +
112                    " FROM " + Tables.STATUS_UPDATES +
113                    " JOIN " + Tables.DATA +
114                    "   ON (" + StatusUpdatesColumns.DATA_ID + "="
115                            + DataColumns.CONCRETE_ID + ")" +
116                    " JOIN " + Tables.RAW_CONTACTS +
117                    "   ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "="
118                            + RawContactsColumns.CONCRETE_ID + ")" +
119                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
120                    " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
121                            + StatusUpdates.STATUS +
122                    " LIMIT 1)" +
123            " WHERE " + ContactsColumns.CONCRETE_ID + "=?";
124
125    // From system/core/logcat/event-log-tags
126    // aggregator [time, count] will be logged for each aggregator cycle.
127    // For the query (as opposed to the merge), count will be negative
128    public static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747;
129
130    // If we encounter more than this many contacts with matching names, aggregate only this many
131    private static final int PRIMARY_HIT_LIMIT = 15;
132    private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT);
133
134    // If we encounter more than this many contacts with matching phone number or email,
135    // don't attempt to aggregate - this is likely an error or a shared corporate data element.
136    private static final int SECONDARY_HIT_LIMIT = 20;
137    private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT);
138
139    // If we encounter more than this many contacts with matching name during aggregation
140    // suggestion lookup, ignore the remaining results.
141    private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100;
142
143    // Return code for the canJoinIntoContact method.
144    private static final int JOIN = 1;
145    private static final int KEEP_SEPARATE = 0;
146    private static final int RE_AGGREGATE = -1;
147
148    private final ContactsProvider2 mContactsProvider;
149    private final ContactsDatabaseHelper mDbHelper;
150    private PhotoPriorityResolver mPhotoPriorityResolver;
151    private final NameSplitter mNameSplitter;
152    private final CommonNicknameCache mCommonNicknameCache;
153
154    private boolean mEnabled = true;
155
156    /** Precompiled sql statement for setting an aggregated presence */
157    private SQLiteStatement mAggregatedPresenceReplace;
158    private SQLiteStatement mPresenceContactIdUpdate;
159    private SQLiteStatement mRawContactCountQuery;
160    private SQLiteStatement mAggregatedPresenceDelete;
161    private SQLiteStatement mMarkForAggregation;
162    private SQLiteStatement mPhotoIdUpdate;
163    private SQLiteStatement mDisplayNameUpdate;
164    private SQLiteStatement mLookupKeyUpdate;
165    private SQLiteStatement mStarredUpdate;
166    private SQLiteStatement mPinnedUpdate;
167    private SQLiteStatement mContactIdAndMarkAggregatedUpdate;
168    private SQLiteStatement mContactIdUpdate;
169    private SQLiteStatement mMarkAggregatedUpdate;
170    private SQLiteStatement mContactUpdate;
171    private SQLiteStatement mContactInsert;
172    private SQLiteStatement mResetPinnedForRawContact;
173
174    private HashMap<Long, Integer> mRawContactsMarkedForAggregation = Maps.newHashMap();
175
176    private String[] mSelectionArgs1 = new String[1];
177    private String[] mSelectionArgs2 = new String[2];
178
179    private long mMimeTypeIdIdentity;
180    private long mMimeTypeIdEmail;
181    private long mMimeTypeIdPhoto;
182    private long mMimeTypeIdPhone;
183    private String mRawContactsQueryByRawContactId;
184    private String mRawContactsQueryByContactId;
185    private StringBuilder mSb = new StringBuilder();
186    private MatchCandidateList mCandidates = new MatchCandidateList();
187    private ContactMatcher mMatcher = new ContactMatcher();
188    private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate();
189
190    /**
191     * Parameter for the suggestion lookup query.
192     */
193    public static final class AggregationSuggestionParameter {
194        public final String kind;
195        public final String value;
196
197        public AggregationSuggestionParameter(String kind, String value) {
198            this.kind = kind;
199            this.value = value;
200        }
201    }
202
203    /**
204     * Captures a potential match for a given name. The matching algorithm
205     * constructs a bunch of NameMatchCandidate objects for various potential matches
206     * and then executes the search in bulk.
207     */
208    private static class NameMatchCandidate {
209        String mName;
210        int mLookupType;
211
212        public NameMatchCandidate(String name, int nameLookupType) {
213            mName = name;
214            mLookupType = nameLookupType;
215        }
216    }
217
218    /**
219     * A list of {@link NameMatchCandidate} that keeps its elements even when the list is
220     * truncated. This is done for optimization purposes to avoid excessive object allocation.
221     */
222    private static class MatchCandidateList {
223        private final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>();
224        private int mCount;
225
226        /**
227         * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists.
228         */
229        public void add(String name, int nameLookupType) {
230            if (mCount >= mList.size()) {
231                mList.add(new NameMatchCandidate(name, nameLookupType));
232            } else {
233                NameMatchCandidate candidate = mList.get(mCount);
234                candidate.mName = name;
235                candidate.mLookupType = nameLookupType;
236            }
237            mCount++;
238        }
239
240        public void clear() {
241            mCount = 0;
242        }
243
244        public boolean isEmpty() {
245            return mCount == 0;
246        }
247    }
248
249    /**
250     * A convenience class used in the algorithm that figures out which of available
251     * display names to use for an aggregate contact.
252     */
253    private static class DisplayNameCandidate {
254        long rawContactId;
255        String displayName;
256        int displayNameSource;
257        boolean verified;
258        boolean writableAccount;
259
260        public DisplayNameCandidate() {
261            clear();
262        }
263
264        public void clear() {
265            rawContactId = -1;
266            displayName = null;
267            displayNameSource = DisplayNameSources.UNDEFINED;
268            verified = false;
269            writableAccount = false;
270        }
271    }
272
273    /**
274     * Constructor.
275     */
276    public ContactAggregator(ContactsProvider2 contactsProvider,
277            ContactsDatabaseHelper contactsDatabaseHelper,
278            PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter,
279            CommonNicknameCache commonNicknameCache) {
280        mContactsProvider = contactsProvider;
281        mDbHelper = contactsDatabaseHelper;
282        mPhotoPriorityResolver = photoPriorityResolver;
283        mNameSplitter = nameSplitter;
284        mCommonNicknameCache = commonNicknameCache;
285
286        SQLiteDatabase db = mDbHelper.getReadableDatabase();
287
288        // Since we have no way of determining which custom status was set last,
289        // we'll just pick one randomly.  We are using MAX as an approximation of randomness
290        final String replaceAggregatePresenceSql =
291                "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
292                + AggregatedPresenceColumns.CONTACT_ID + ", "
293                + StatusUpdates.PRESENCE + ", "
294                + StatusUpdates.CHAT_CAPABILITY + ")"
295                + " SELECT " + PresenceColumns.CONTACT_ID + ","
296                + StatusUpdates.PRESENCE + ","
297                + StatusUpdates.CHAT_CAPABILITY
298                + " FROM " + Tables.PRESENCE
299                + " WHERE "
300                + " (" + StatusUpdates.PRESENCE
301                +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
302                + " = (SELECT "
303                + "MAX (" + StatusUpdates.PRESENCE
304                +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
305                + " FROM " + Tables.PRESENCE
306                + " WHERE " + PresenceColumns.CONTACT_ID
307                + "=?)"
308                + " AND " + PresenceColumns.CONTACT_ID
309                + "=?;";
310        mAggregatedPresenceReplace = db.compileStatement(replaceAggregatePresenceSql);
311
312        mRawContactCountQuery = db.compileStatement(
313                "SELECT COUNT(" + RawContacts._ID + ")" +
314                " FROM " + Tables.RAW_CONTACTS +
315                " WHERE " + RawContacts.CONTACT_ID + "=?"
316                        + " AND " + RawContacts._ID + "<>?");
317
318        mAggregatedPresenceDelete = db.compileStatement(
319                "DELETE FROM " + Tables.AGGREGATED_PRESENCE +
320                " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?");
321
322        mMarkForAggregation = db.compileStatement(
323                "UPDATE " + Tables.RAW_CONTACTS +
324                " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" +
325                " WHERE " + RawContacts._ID + "=?"
326                        + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0");
327
328        mPhotoIdUpdate = db.compileStatement(
329                "UPDATE " + Tables.CONTACTS +
330                " SET " + Contacts.PHOTO_ID + "=?," + Contacts.PHOTO_FILE_ID + "=? " +
331                " WHERE " + Contacts._ID + "=?");
332
333        mDisplayNameUpdate = db.compileStatement(
334                "UPDATE " + Tables.CONTACTS +
335                " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " +
336                " WHERE " + Contacts._ID + "=?");
337
338        mLookupKeyUpdate = db.compileStatement(
339                "UPDATE " + Tables.CONTACTS +
340                " SET " + Contacts.LOOKUP_KEY + "=? " +
341                " WHERE " + Contacts._ID + "=?");
342
343        mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
344                + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED
345                + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE "
346                + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND "
347                + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?");
348
349        mPinnedUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
350                + Contacts.PINNED + " = IFNULL((SELECT MIN(" + RawContacts.PINNED + ") FROM "
351                + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "="
352                + ContactsColumns.CONCRETE_ID + " AND " + RawContacts.PINNED + ">"
353                + PinnedPositions.UNPINNED + ")," + PinnedPositions.UNPINNED + ") "
354                + "WHERE " + Contacts._ID + "=?");
355
356        mContactIdAndMarkAggregatedUpdate = db.compileStatement(
357                "UPDATE " + Tables.RAW_CONTACTS +
358                " SET " + RawContacts.CONTACT_ID + "=?, "
359                        + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
360                " WHERE " + RawContacts._ID + "=?");
361
362        mContactIdUpdate = db.compileStatement(
363                "UPDATE " + Tables.RAW_CONTACTS +
364                " SET " + RawContacts.CONTACT_ID + "=?" +
365                " WHERE " + RawContacts._ID + "=?");
366
367        mMarkAggregatedUpdate = db.compileStatement(
368                "UPDATE " + Tables.RAW_CONTACTS +
369                " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
370                " WHERE " + RawContacts._ID + "=?");
371
372        mPresenceContactIdUpdate = db.compileStatement(
373                "UPDATE " + Tables.PRESENCE +
374                " SET " + PresenceColumns.CONTACT_ID + "=?" +
375                " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?");
376
377        mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL);
378        mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL);
379
380        mResetPinnedForRawContact = db.compileStatement(
381                "UPDATE " + Tables.RAW_CONTACTS +
382                " SET " + RawContacts.PINNED + "=" + PinnedPositions.UNPINNED +
383                " WHERE " + RawContacts._ID + "=?");
384
385        mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
386        mMimeTypeIdIdentity = mDbHelper.getMimeTypeId(Identity.CONTENT_ITEM_TYPE);
387        mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
388        mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
389
390        // Query used to retrieve data from raw contacts to populate the corresponding aggregate
391        mRawContactsQueryByRawContactId = String.format(Locale.US,
392                RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID,
393                mMimeTypeIdPhoto, mMimeTypeIdPhone);
394
395        mRawContactsQueryByContactId = String.format(Locale.US,
396                RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
397                mMimeTypeIdPhoto, mMimeTypeIdPhone);
398    }
399
400    public void setEnabled(boolean enabled) {
401        mEnabled = enabled;
402    }
403
404    public boolean isEnabled() {
405        return mEnabled;
406    }
407
408    private interface AggregationQuery {
409        String SQL =
410                "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
411                        ", " + RawContactsColumns.ACCOUNT_ID +
412                " FROM " + Tables.RAW_CONTACTS +
413                " WHERE " + RawContacts._ID + " IN(";
414
415        int _ID = 0;
416        int CONTACT_ID = 1;
417        int ACCOUNT_ID = 2;
418    }
419
420    /**
421     * Aggregate all raw contacts that were marked for aggregation in the current transaction.
422     * Call just before committing the transaction.
423     */
424    public void aggregateInTransaction(TransactionContext txContext, SQLiteDatabase db) {
425        final int markedCount = mRawContactsMarkedForAggregation.size();
426        if (markedCount == 0) {
427            return;
428        }
429
430        final long start = System.currentTimeMillis();
431        if (DEBUG_LOGGING) {
432            Log.d(TAG, "aggregateInTransaction for " + markedCount + " contacts");
433        }
434
435        EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -markedCount);
436
437        int index = 0;
438
439        // We don't use the cached string builder (namely mSb)  here, as this string can be very
440        // long when upgrading (where we re-aggregate all visible contacts) and StringBuilder won't
441        // shrink the internal storage.
442        // Note: don't use selection args here.  We just include all IDs directly in the selection,
443        // because there's a limit for the number of parameters in a query.
444        final StringBuilder sbQuery = new StringBuilder();
445        sbQuery.append(AggregationQuery.SQL);
446        for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) {
447            if (index > 0) {
448                sbQuery.append(',');
449            }
450            sbQuery.append(rawContactId);
451            index++;
452        }
453
454        sbQuery.append(')');
455
456        final long[] rawContactIds;
457        final long[] contactIds;
458        final long[] accountIds;
459        final int actualCount;
460        final Cursor c = db.rawQuery(sbQuery.toString(), null);
461        try {
462            actualCount = c.getCount();
463            rawContactIds = new long[actualCount];
464            contactIds = new long[actualCount];
465            accountIds = new long[actualCount];
466
467            index = 0;
468            while (c.moveToNext()) {
469                rawContactIds[index] = c.getLong(AggregationQuery._ID);
470                contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
471                accountIds[index] = c.getLong(AggregationQuery.ACCOUNT_ID);
472                index++;
473            }
474        } finally {
475            c.close();
476        }
477
478        if (DEBUG_LOGGING) {
479            Log.d(TAG, "aggregateInTransaction: initial query done.");
480        }
481
482        for (int i = 0; i < actualCount; i++) {
483            aggregateContact(txContext, db, rawContactIds[i], accountIds[i], contactIds[i],
484                    mCandidates, mMatcher);
485        }
486
487        long elapsedTime = System.currentTimeMillis() - start;
488        EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, actualCount);
489
490        if (DEBUG_LOGGING) {
491            Log.d(TAG, "Contact aggregation complete: " + actualCount +
492                    (actualCount == 0 ? "" : ", " + (elapsedTime / actualCount)
493                            + " ms per raw contact"));
494        }
495    }
496
497    @SuppressWarnings("deprecation")
498    public void triggerAggregation(TransactionContext txContext, long rawContactId) {
499        if (!mEnabled) {
500            return;
501        }
502
503        int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
504        switch (aggregationMode) {
505            case RawContacts.AGGREGATION_MODE_DISABLED:
506                break;
507
508            case RawContacts.AGGREGATION_MODE_DEFAULT: {
509                markForAggregation(rawContactId, aggregationMode, false);
510                break;
511            }
512
513            case RawContacts.AGGREGATION_MODE_SUSPENDED: {
514                long contactId = mDbHelper.getContactId(rawContactId);
515
516                if (contactId != 0) {
517                    updateAggregateData(txContext, contactId);
518                }
519                break;
520            }
521
522            case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
523                aggregateContact(txContext, mDbHelper.getWritableDatabase(), rawContactId);
524                break;
525            }
526        }
527    }
528
529    public void clearPendingAggregations() {
530        // HashMap woulnd't shrink the internal table once expands it, so let's just re-create
531        // a new one instead of clear()ing it.
532        mRawContactsMarkedForAggregation = Maps.newHashMap();
533    }
534
535    public void markNewForAggregation(long rawContactId, int aggregationMode) {
536        mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode);
537    }
538
539    public void markForAggregation(long rawContactId, int aggregationMode, boolean force) {
540        final int effectiveAggregationMode;
541        if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) {
542            // As per ContactsContract documentation, default aggregation mode
543            // does not override a previously set mode
544            if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
545                effectiveAggregationMode = mRawContactsMarkedForAggregation.get(rawContactId);
546            } else {
547                effectiveAggregationMode = aggregationMode;
548            }
549        } else {
550            mMarkForAggregation.bindLong(1, rawContactId);
551            mMarkForAggregation.execute();
552            effectiveAggregationMode = aggregationMode;
553        }
554
555        mRawContactsMarkedForAggregation.put(rawContactId, effectiveAggregationMode);
556    }
557
558    private static class RawContactIdAndAggregationModeQuery {
559        public static final String TABLE = Tables.RAW_CONTACTS;
560
561        public static final String[] COLUMNS = { RawContacts._ID, RawContacts.AGGREGATION_MODE };
562
563        public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
564
565        public static final int _ID = 0;
566        public static final int AGGREGATION_MODE = 1;
567    }
568
569    /**
570     * Marks all constituent raw contacts of an aggregated contact for re-aggregation.
571     */
572    private void markContactForAggregation(SQLiteDatabase db, long contactId) {
573        mSelectionArgs1[0] = String.valueOf(contactId);
574        Cursor cursor = db.query(RawContactIdAndAggregationModeQuery.TABLE,
575                RawContactIdAndAggregationModeQuery.COLUMNS,
576                RawContactIdAndAggregationModeQuery.SELECTION, mSelectionArgs1, null, null, null);
577        try {
578            if (cursor.moveToFirst()) {
579                long rawContactId = cursor.getLong(RawContactIdAndAggregationModeQuery._ID);
580                int aggregationMode = cursor.getInt(
581                        RawContactIdAndAggregationModeQuery.AGGREGATION_MODE);
582                // Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED.
583                // (Also just ignore deprecated AGGREGATION_MODE_IMMEDIATE)
584                if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
585                    markForAggregation(rawContactId, aggregationMode, true);
586                }
587            }
588        } finally {
589            cursor.close();
590        }
591    }
592
593    /**
594     * Mark all visible contacts for re-aggregation.
595     *
596     * - Set {@link RawContactsColumns#AGGREGATION_NEEDED} For all visible raw_contacts with
597     *   {@link RawContacts#AGGREGATION_MODE_DEFAULT}.
598     * - Also put them into {@link #mRawContactsMarkedForAggregation}.
599     */
600    public int markAllVisibleForAggregation(SQLiteDatabase db) {
601        final long start = System.currentTimeMillis();
602
603        // Set AGGREGATION_NEEDED for all visible raw_cotnacts with AGGREGATION_MODE_DEFAULT.
604        // (Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED)
605        db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " +
606                RawContactsColumns.AGGREGATION_NEEDED + "=1" +
607                " WHERE " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY +
608                " AND " + RawContacts.AGGREGATION_MODE + "=" + RawContacts.AGGREGATION_MODE_DEFAULT
609                );
610
611        final int count;
612        final Cursor cursor = db.rawQuery("SELECT " + RawContacts._ID +
613                " FROM " + Tables.RAW_CONTACTS +
614                " WHERE " + RawContactsColumns.AGGREGATION_NEEDED + "=1", null);
615        try {
616            count = cursor.getCount();
617            cursor.moveToPosition(-1);
618            while (cursor.moveToNext()) {
619                final long rawContactId = cursor.getLong(0);
620                mRawContactsMarkedForAggregation.put(rawContactId,
621                        RawContacts.AGGREGATION_MODE_DEFAULT);
622            }
623        } finally {
624            cursor.close();
625        }
626
627        final long end = System.currentTimeMillis();
628        Log.i(TAG, "Marked all visible contacts for aggregation: " + count + " raw contacts, " +
629                (end - start) + " ms");
630        return count;
631    }
632
633    /**
634     * Creates a new contact based on the given raw contact.  Does not perform aggregation.  Returns
635     * the ID of the contact that was created.
636     */
637    public long onRawContactInsert(
638            TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
639        long contactId = insertContact(db, rawContactId);
640        setContactId(rawContactId, contactId);
641        mDbHelper.updateContactVisible(txContext, contactId);
642        return contactId;
643    }
644
645    protected long insertContact(SQLiteDatabase db, long rawContactId) {
646        mSelectionArgs1[0] = String.valueOf(rawContactId);
647        computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
648        return mContactInsert.executeInsert();
649    }
650
651    private static final class RawContactIdAndAccountQuery {
652        public static final String TABLE = Tables.RAW_CONTACTS;
653
654        public static final String[] COLUMNS = {
655                RawContacts.CONTACT_ID,
656                RawContactsColumns.ACCOUNT_ID
657        };
658
659        public static final String SELECTION = RawContacts._ID + "=?";
660
661        public static final int CONTACT_ID = 0;
662        public static final int ACCOUNT_ID = 1;
663    }
664
665    public void aggregateContact(
666            TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
667        if (!mEnabled) {
668            return;
669        }
670
671        MatchCandidateList candidates = new MatchCandidateList();
672        ContactMatcher matcher = new ContactMatcher();
673
674        long contactId = 0;
675        long accountId = 0;
676        mSelectionArgs1[0] = String.valueOf(rawContactId);
677        Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE,
678                RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION,
679                mSelectionArgs1, null, null, null);
680        try {
681            if (cursor.moveToFirst()) {
682                contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID);
683                accountId = cursor.getLong(RawContactIdAndAccountQuery.ACCOUNT_ID);
684            }
685        } finally {
686            cursor.close();
687        }
688
689        aggregateContact(txContext, db, rawContactId, accountId, contactId,
690                candidates, matcher);
691    }
692
693    public void updateAggregateData(TransactionContext txContext, long contactId) {
694        if (!mEnabled) {
695            return;
696        }
697
698        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
699        computeAggregateData(db, contactId, mContactUpdate);
700        mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
701        mContactUpdate.execute();
702
703        mDbHelper.updateContactVisible(txContext, contactId);
704        updateAggregatedStatusUpdate(contactId);
705    }
706
707    private void updateAggregatedStatusUpdate(long contactId) {
708        mAggregatedPresenceReplace.bindLong(1, contactId);
709        mAggregatedPresenceReplace.bindLong(2, contactId);
710        mAggregatedPresenceReplace.execute();
711        updateLastStatusUpdateId(contactId);
712    }
713
714    /**
715     * Adjusts the reference to the latest status update for the specified contact.
716     */
717    public void updateLastStatusUpdateId(long contactId) {
718        String contactIdString = String.valueOf(contactId);
719        mDbHelper.getWritableDatabase().execSQL(UPDATE_LAST_STATUS_UPDATE_ID_SQL,
720                new String[]{contactIdString, contactIdString});
721    }
722
723    /**
724     * Given a specific raw contact, finds all matching aggregate contacts and chooses the one
725     * with the highest match score.  If no such contact is found, creates a new contact.
726     */
727    private synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db,
728            long rawContactId, long accountId, long currentContactId, MatchCandidateList candidates,
729            ContactMatcher matcher) {
730
731        if (VERBOSE_LOGGING) {
732            Log.v(TAG, "aggregateContact: rid=" + rawContactId + " cid=" + currentContactId);
733        }
734
735        int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
736
737        Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId);
738        if (aggModeObject != null) {
739            aggregationMode = aggModeObject;
740        }
741
742        long contactId = -1; // Best matching contact ID.
743        boolean needReaggregate = false;
744
745        final Set<Long> rawContactIdsInSameAccount = new HashSet<Long>();
746        final Set<Long> rawContactIdsInOtherAccount = new HashSet<Long>();
747        if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
748            candidates.clear();
749            matcher.clear();
750
751            contactId = pickBestMatchBasedOnExceptions(db, rawContactId, matcher);
752            if (contactId == -1) {
753
754                // If this is a newly inserted contact or a visible contact, look for
755                // data matches.
756                if (currentContactId == 0
757                        || mDbHelper.isContactInDefaultDirectory(db, currentContactId)) {
758                    contactId = pickBestMatchBasedOnData(db, rawContactId, candidates, matcher);
759                }
760
761                // If we found an best matched contact, find out if the raw contact can be joined
762                // into it
763                if (contactId != -1 && contactId != currentContactId) {
764                    // List all raw contact ID and their account ID mappings in contact
765                    // [contactId] excluding raw_contact [rawContactId].
766
767                    // Based on the mapping, create two sets of raw contact IDs in
768                    // [rawContactAccountId] and not in [rawContactAccountId]. We don't always
769                    // need them, so lazily initialize them.
770                    mSelectionArgs2[0] = String.valueOf(contactId);
771                    mSelectionArgs2[1] = String.valueOf(rawContactId);
772                    final Cursor rawContactsToAccountsCursor = db.rawQuery(
773                            "SELECT " + RawContacts._ID + ", " + RawContactsColumns.ACCOUNT_ID +
774                                    " FROM " + Tables.RAW_CONTACTS +
775                                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
776                                    " AND " + RawContacts._ID + "!=?",
777                            mSelectionArgs2);
778                    try {
779                        rawContactsToAccountsCursor.moveToPosition(-1);
780                        while (rawContactsToAccountsCursor.moveToNext()) {
781                            final long rcId = rawContactsToAccountsCursor.getLong(0);
782                            final long rc_accountId = rawContactsToAccountsCursor.getLong(1);
783                            if (rc_accountId == accountId) {
784                                rawContactIdsInSameAccount.add(rcId);
785                            } else {
786                                rawContactIdsInOtherAccount.add(rcId);
787                            }
788                        }
789                    } finally {
790                        rawContactsToAccountsCursor.close();
791                    }
792                    final int actionCode = canJoinIntoContact(db, rawContactId,
793                            rawContactIdsInSameAccount, rawContactIdsInOtherAccount);
794                    if (actionCode == KEEP_SEPARATE) {
795                        contactId = -1;
796                    } else if (actionCode == RE_AGGREGATE) {
797                        needReaggregate = true;
798                    }
799                }
800            }
801        } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
802            return;
803        }
804
805        // # of raw_contacts in the [currentContactId] contact excluding the [rawContactId]
806        // raw_contact.
807        long currentContactContentsCount = 0;
808
809        if (currentContactId != 0) {
810            mRawContactCountQuery.bindLong(1, currentContactId);
811            mRawContactCountQuery.bindLong(2, rawContactId);
812            currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong();
813        }
814
815        // If there are no other raw contacts in the current aggregate, we might as well reuse it.
816        // Also, if the aggregation mode is SUSPENDED, we must reuse the same aggregate.
817        if (contactId == -1
818                && currentContactId != 0
819                && (currentContactContentsCount == 0
820                        || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) {
821            contactId = currentContactId;
822        }
823
824        if (contactId == currentContactId) {
825            // Aggregation unchanged
826            markAggregated(rawContactId);
827            if (VERBOSE_LOGGING) {
828                Log.v(TAG, "Aggregation unchanged");
829            }
830        } else if (contactId == -1) {
831            // create new contact for [rawContactId]
832            createContactForRawContacts(db, txContext, Sets.newHashSet(rawContactId), null);
833            if (currentContactContentsCount > 0) {
834                updateAggregateData(txContext, currentContactId);
835            }
836            if (VERBOSE_LOGGING) {
837                Log.v(TAG, "create new contact for rid=" + rawContactId);
838            }
839        } else if (needReaggregate) {
840            // re-aggregate
841            final Set<Long> allRawContactIdSet = new HashSet<Long>();
842            allRawContactIdSet.addAll(rawContactIdsInSameAccount);
843            allRawContactIdSet.addAll(rawContactIdsInOtherAccount);
844            // If there is no other raw contacts aggregated with the given raw contact currently,
845            // we might as well reuse it.
846            currentContactId = (currentContactId != 0 && currentContactContentsCount == 0)
847                    ? currentContactId : 0;
848            reAggregateRawContacts(txContext, db, contactId, currentContactId, rawContactId,
849                    allRawContactIdSet);
850            if (VERBOSE_LOGGING) {
851                Log.v(TAG, "Re-aggregating rid=" + rawContactId + " and cid=" + contactId);
852            }
853        } else {
854            // Joining with an existing aggregate
855            if (currentContactContentsCount == 0) {
856                // Delete a previous aggregate if it only contained this raw contact
857                ContactsTableUtil.deleteContact(db, currentContactId);
858
859                mAggregatedPresenceDelete.bindLong(1, currentContactId);
860                mAggregatedPresenceDelete.execute();
861            }
862
863            clearSuperPrimarySetting(db, contactId, rawContactId);
864            setContactIdAndMarkAggregated(rawContactId, contactId);
865            computeAggregateData(db, contactId, mContactUpdate);
866            mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
867            mContactUpdate.execute();
868            mDbHelper.updateContactVisible(txContext, contactId);
869            updateAggregatedStatusUpdate(contactId);
870            // Make sure the raw contact does not contribute to the current contact
871            if (currentContactId != 0) {
872                updateAggregateData(txContext, currentContactId);
873            }
874            if (VERBOSE_LOGGING) {
875                Log.v(TAG, "Join rid=" + rawContactId + " with cid=" + contactId);
876            }
877        }
878    }
879
880    /**
881     * Find out which mime-types are shared by raw contact of {@code rawContactId} and raw contacts
882     * of {@code contactId}. Clear the is_super_primary settings for these mime-types.
883     */
884    private void clearSuperPrimarySetting(SQLiteDatabase db, long contactId, long rawContactId) {
885        final String[] args = {String.valueOf(contactId), String.valueOf(rawContactId)};
886
887        // Find out which mime-types are shared by raw contact of rawContactId and raw contacts
888        // of contactId
889        int index = 0;
890        final StringBuilder mimeTypeCondition = new StringBuilder();
891        mimeTypeCondition.append(" AND " + DataColumns.MIMETYPE_ID + " IN (");
892
893        final Cursor c = db.rawQuery(
894                "SELECT DISTINCT(a." + DataColumns.MIMETYPE_ID + ")" +
895                " FROM (SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA + " WHERE " +
896                        Data.RAW_CONTACT_ID + " IN (SELECT " + RawContacts._ID + " FROM " +
897                        Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=?1)) AS a" +
898                " JOIN  (SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA + " WHERE "
899                        + Data.RAW_CONTACT_ID + "=?2) AS b" +
900                " ON a." + DataColumns.MIMETYPE_ID + "=b." + DataColumns.MIMETYPE_ID,
901                args);
902        try {
903            c.moveToPosition(-1);
904            while (c.moveToNext()) {
905                if (index > 0) {
906                    mimeTypeCondition.append(',');
907                }
908                mimeTypeCondition.append(c.getLong((0)));
909                index++;
910            }
911        } finally {
912            c.close();
913        }
914
915        // Clear is_super_primary setting for all the mime-types exist in both raw contact
916        // of rawContactId and raw contacts of contactId
917        String superPrimaryUpdateSql = "UPDATE " + Tables.DATA +
918                " SET " + Data.IS_SUPER_PRIMARY + "=0" +
919                " WHERE (" +  Data.RAW_CONTACT_ID +
920                        " IN (SELECT " + RawContacts._ID +  " FROM " + Tables.RAW_CONTACTS +
921                        " WHERE " + RawContacts.CONTACT_ID + "=?1)" +
922                        " OR " +  Data.RAW_CONTACT_ID + "=?2)";
923
924        if (index > 0) {
925            mimeTypeCondition.append(')');
926            superPrimaryUpdateSql += mimeTypeCondition.toString();
927        }
928
929        db.execSQL(superPrimaryUpdateSql, args);
930    }
931
932    /**
933     * @return JOIN if the raw contact of {@code rawContactId} can be joined into the existing
934     * contact of {@code contactId}. KEEP_SEPARATE if the raw contact of {@code rawContactId}
935     * cannot be joined into the existing contact of {@code contactId}. RE_AGGREGATE if raw contact
936     * of {@code rawContactId} and all the raw contacts of contact of {@code contactId} need to be
937     * re-aggregated.
938     *
939     * If contact of {@code contactId} doesn't contain any raw contacts from the same account as
940     * raw contact of {@code rawContactId}, join raw contact with contact if there is no identity
941     * mismatch between them on the same namespace, otherwise, keep them separate.
942     *
943     * If contact of {@code contactId} contains raw contacts from the same account as raw contact of
944     * {@code rawContactId}, join raw contact with contact if there's at least one raw contact in
945     * those raw contacts that shares at least one email address, phone number, or identity;
946     * otherwise, re-aggregate raw contact and all the raw contacts of contact.
947     */
948    private int canJoinIntoContact(SQLiteDatabase db, long rawContactId,
949            Set<Long> rawContactIdsInSameAccount, Set<Long> rawContactIdsInOtherAccount ) {
950
951        if (rawContactIdsInSameAccount.isEmpty()) {
952            final String rid = String.valueOf(rawContactId);
953            final String ridsInOtherAccts = TextUtils.join(",", rawContactIdsInOtherAccount);
954            // If there is no identity match between raw contact of [rawContactId] and
955            // any raw contact in other accounts on the same namespace, and there is at least
956            // one identity mismatch exist, keep raw contact separate from contact.
957            if (DatabaseUtils.longForQuery(db, buildIdentityMatchingSql(rid, ridsInOtherAccts,
958                    /* isIdentityMatching =*/ true, /* countOnly =*/ true), null) == 0 &&
959                    DatabaseUtils.longForQuery(db, buildIdentityMatchingSql(rid, ridsInOtherAccts,
960                            /* isIdentityMatching =*/ false, /* countOnly =*/ true), null) > 0) {
961                if (VERBOSE_LOGGING) {
962                    Log.v(TAG, "canJoinIntoContact: no duplicates, but has no matching identity " +
963                            "and has mis-matching identity on the same namespace between rid=" +
964                            rid + " and ridsInOtherAccts=" + ridsInOtherAccts);
965                }
966                return KEEP_SEPARATE; // has identity and identity doesn't match
967            } else {
968                if (VERBOSE_LOGGING) {
969                    Log.v(TAG, "canJoinIntoContact: can join the first raw contact from the same " +
970                            "account without any identity mismatch.");
971                }
972                return JOIN; // no identity or identity match
973            }
974        }
975        if (VERBOSE_LOGGING) {
976            Log.v(TAG, "canJoinIntoContact: " + rawContactIdsInSameAccount.size() +
977                    " duplicate(s) found");
978        }
979
980
981        final Set<Long> rawContactIdSet = new HashSet<Long>();
982        rawContactIdSet.add(rawContactId);
983        if (rawContactIdsInSameAccount.size() > 0 &&
984                isDataMaching(db, rawContactIdSet, rawContactIdsInSameAccount)) {
985            if (VERBOSE_LOGGING) {
986                Log.v(TAG, "canJoinIntoContact: join if there is a data matching found in the " +
987                        "same account");
988            }
989            return JOIN;
990        } else {
991            if (VERBOSE_LOGGING) {
992                Log.v(TAG, "canJoinIntoContact: re-aggregate rid=" + rawContactId +
993                        " with its best matching contact to connected component");
994            }
995            return RE_AGGREGATE;
996        }
997    }
998
999    private interface RawContactMatchingSelectionStatement {
1000        String SELECT_COUNT =  "SELECT count(*) " ;
1001        String SELECT_ID = "SELECT d1." + Data.RAW_CONTACT_ID + ",d2."  + Data.RAW_CONTACT_ID ;
1002    }
1003
1004    /**
1005     * Build sql to check if there is any identity match/mis-match between two sets of raw contact
1006     * ids on the same namespace.
1007     */
1008    private String buildIdentityMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
1009            boolean isIdentityMatching, boolean countOnly) {
1010        final String identityType = String.valueOf(mMimeTypeIdIdentity);
1011        final String matchingOperator = (isIdentityMatching) ? "=" : "!=";
1012        final String sql =
1013                " FROM " + Tables.DATA + " AS d1" +
1014                " JOIN " + Tables.DATA + " AS d2" +
1015                        " ON (d1." + Identity.IDENTITY + matchingOperator +
1016                        " d2." + Identity.IDENTITY + " AND" +
1017                        " d1." + Identity.NAMESPACE + " = d2." + Identity.NAMESPACE + " )" +
1018                " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + identityType +
1019                " AND d2." + DataColumns.MIMETYPE_ID + " = " + identityType +
1020                " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
1021                " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
1022        return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
1023                RawContactMatchingSelectionStatement.SELECT_ID + sql;
1024    }
1025
1026    private String buildEmailMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
1027            boolean countOnly) {
1028        final String emailType = String.valueOf(mMimeTypeIdEmail);
1029        final String sql =
1030                " FROM " + Tables.DATA + " AS d1" +
1031                " JOIN " + Tables.DATA + " AS d2" +
1032                        " ON lower(d1." + Email.ADDRESS + ")= lower(d2." + Email.ADDRESS + ")" +
1033                " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + emailType +
1034                " AND d2." + DataColumns.MIMETYPE_ID + " = " + emailType +
1035                " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
1036                " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
1037        return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
1038                RawContactMatchingSelectionStatement.SELECT_ID + sql;
1039    }
1040
1041    private String buildPhoneMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
1042            boolean countOnly) {
1043        // It's a bit tricker because it has to be consistent with
1044        // updateMatchScoresBasedOnPhoneMatches().
1045        final String phoneType = String.valueOf(mMimeTypeIdPhone);
1046        final String sql =
1047                " FROM " + Tables.PHONE_LOOKUP + " AS p1" +
1048                " JOIN " + Tables.DATA + " AS d1 ON " +
1049                        "(d1." + Data._ID + "=p1." + PhoneLookupColumns.DATA_ID + ")" +
1050                " JOIN " + Tables.PHONE_LOOKUP + " AS p2 ON (p1." + PhoneLookupColumns.MIN_MATCH +
1051                        "=p2." + PhoneLookupColumns.MIN_MATCH + ")" +
1052                " JOIN " + Tables.DATA + " AS d2 ON " +
1053                        "(d2." + Data._ID + "=p2." + PhoneLookupColumns.DATA_ID + ")" +
1054                " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + phoneType +
1055                " AND d2." + DataColumns.MIMETYPE_ID + " = " + phoneType +
1056                " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
1057                " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")" +
1058                " AND PHONE_NUMBERS_EQUAL(d1." + Phone.NUMBER + ",d2." + Phone.NUMBER + "," +
1059                        String.valueOf(mDbHelper.getUseStrictPhoneNumberComparisonParameter()) +
1060                        ")";
1061        return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
1062                RawContactMatchingSelectionStatement.SELECT_ID + sql;
1063    }
1064
1065    private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2) {
1066        return "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " +
1067                AggregationExceptions.RAW_CONTACT_ID2 +
1068                " FROM " + Tables.AGGREGATION_EXCEPTIONS +
1069                " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" +
1070                        rawContactIdSet1 + ")" +
1071                " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" +
1072                " AND " + AggregationExceptions.TYPE + "=" +
1073                        AggregationExceptions.TYPE_KEEP_TOGETHER ;
1074    }
1075
1076    private boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query) {
1077        return DatabaseUtils.longForQuery(db, query, null) > 0;
1078    }
1079
1080    /**
1081     * If there's any identity, email address or a phone number matching between two raw contact
1082     * sets.
1083     */
1084    private boolean isDataMaching(SQLiteDatabase db, Set<Long> rawContactIdSet1,
1085            Set<Long> rawContactIdSet2) {
1086        final String rawContactIds1 = TextUtils.join(",", rawContactIdSet1);
1087        final String rawContactIds2 = TextUtils.join(",", rawContactIdSet2);
1088        // First, check for the identity
1089        if (isFirstColumnGreaterThanZero(db, buildIdentityMatchingSql(
1090                rawContactIds1, rawContactIds2,  /* isIdentityMatching =*/ true,
1091                /* countOnly =*/true))) {
1092            if (VERBOSE_LOGGING) {
1093                Log.v(TAG, "canJoinIntoContact: identity match found between " + rawContactIds1 +
1094                        " and " + rawContactIds2);
1095            }
1096            return true;
1097        }
1098
1099        // Next, check for the email address.
1100        if (isFirstColumnGreaterThanZero(db,
1101                buildEmailMatchingSql(rawContactIds1, rawContactIds2, true))) {
1102            if (VERBOSE_LOGGING) {
1103                Log.v(TAG, "canJoinIntoContact: email match found between " + rawContactIds1 +
1104                        " and " + rawContactIds2);
1105            }
1106            return true;
1107        }
1108
1109        // Lastly, the phone number.
1110        if (isFirstColumnGreaterThanZero(db,
1111                buildPhoneMatchingSql(rawContactIds1, rawContactIds2, true))) {
1112            if (VERBOSE_LOGGING) {
1113                Log.v(TAG, "canJoinIntoContact: phone match found between " + rawContactIds1 +
1114                        " and " + rawContactIds2);
1115            }
1116            return true;
1117        }
1118        return false;
1119    }
1120
1121    /**
1122     * Re-aggregate rawContact of {@code rawContactId} and all the raw contacts of
1123     * {@code existingRawContactIds} into connected components. This only happens when a given
1124     * raw contacts cannot be joined with its best matching contacts directly.
1125     *
1126     *  Two raw contacts are considered connected if they share at least one email address, phone
1127     *  number or identity. Create new contact for each connected component except the very first
1128     *  one that doesn't contain rawContactId of {@code rawContactId}.
1129     */
1130    private void reAggregateRawContacts(TransactionContext txContext, SQLiteDatabase db,
1131            long contactId, long currentContactId, long rawContactId,
1132            Set<Long> existingRawContactIds) {
1133        // Find the connected component based on the aggregation exceptions or
1134        // identity/email/phone matching for all the raw contacts of [contactId] and the give
1135        // raw contact.
1136        final Set<Long> allIds = new HashSet<Long>();
1137        allIds.add(rawContactId);
1138        allIds.addAll(existingRawContactIds);
1139        final Set<Set<Long>> connectedRawContactSets = findConnectedRawContacts(db, allIds);
1140
1141        if (connectedRawContactSets.size() == 1) {
1142            // If everything is connected, create one contact with [contactId]
1143            createContactForRawContacts(db, txContext, connectedRawContactSets.iterator().next(),
1144                    contactId);
1145        } else {
1146            for (Set<Long> connectedRawContactIds : connectedRawContactSets) {
1147                if (connectedRawContactIds.contains(rawContactId)) {
1148                    // crate contact for connect component containing [rawContactId], reuse
1149                    // [currentContactId] if possible.
1150                    createContactForRawContacts(db, txContext, connectedRawContactIds,
1151                            currentContactId == 0 ? null : currentContactId);
1152                    connectedRawContactSets.remove(connectedRawContactIds);
1153                    break;
1154                }
1155            }
1156            // Create new contact for each connected component except the last one. The last one
1157            // will reuse [contactId]. Only the last one can reuse [contactId] when all other raw
1158            // contacts has already been assigned new contact Id, so that the contact aggregation
1159            // stats could be updated correctly.
1160            int index = connectedRawContactSets.size();
1161            for (Set<Long> connectedRawContactIds : connectedRawContactSets) {
1162                if (index > 1) {
1163                    createContactForRawContacts(db, txContext, connectedRawContactIds, null);
1164                    index--;
1165                } else {
1166                    createContactForRawContacts(db, txContext, connectedRawContactIds, contactId);
1167                }
1168            }
1169        }
1170    }
1171
1172    /**
1173     * Partition the given raw contact Ids to connected component based on aggregation exception,
1174     * identity matching, email matching or phone matching.
1175     */
1176    private Set<Set<Long>> findConnectedRawContacts(SQLiteDatabase db, Set<Long> rawContactIdSet) {
1177        // Connections between two raw contacts
1178       final Multimap<Long, Long> matchingRawIdPairs = HashMultimap.create();
1179        String rawContactIds = TextUtils.join(",", rawContactIdSet);
1180        findIdPairs(db, buildExceptionMatchingSql(rawContactIds, rawContactIds),
1181                matchingRawIdPairs);
1182        findIdPairs(db, buildIdentityMatchingSql(rawContactIds, rawContactIds,
1183                /* isIdentityMatching =*/ true, /* countOnly =*/false), matchingRawIdPairs);
1184        findIdPairs(db, buildEmailMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false),
1185                matchingRawIdPairs);
1186        findIdPairs(db, buildPhoneMatchingSql(rawContactIds, rawContactIds,  /* countOnly =*/false),
1187                matchingRawIdPairs);
1188
1189        return findConnectedComponents(rawContactIdSet, matchingRawIdPairs);
1190    }
1191
1192    /**
1193     * Given a set of raw contact ids {@code rawContactIdSet} and the connection among them
1194     * {@code matchingRawIdPairs}, find the connected components.
1195     */
1196    @VisibleForTesting
1197    static Set<Set<Long>> findConnectedComponents(Set<Long> rawContactIdSet, Multimap<Long,
1198            Long> matchingRawIdPairs) {
1199        Set<Set<Long>> connectedRawContactSets = new HashSet<Set<Long>>();
1200        Set<Long> visited = new HashSet<Long>();
1201        for (Long id : rawContactIdSet) {
1202            if (!visited.contains(id)) {
1203                Set<Long> set = new HashSet<Long>();
1204                findConnectedComponentForRawContact(matchingRawIdPairs, visited, id, set);
1205                connectedRawContactSets.add(set);
1206            }
1207        }
1208        return connectedRawContactSets;
1209    }
1210
1211    private static void findConnectedComponentForRawContact(Multimap<Long, Long> connections,
1212            Set<Long> visited, Long rawContactId, Set<Long> results) {
1213        visited.add(rawContactId);
1214        results.add(rawContactId);
1215        for (long match : connections.get(rawContactId)) {
1216            if (!visited.contains(match)) {
1217                findConnectedComponentForRawContact(connections, visited, match, results);
1218            }
1219        }
1220    }
1221
1222    /**
1223     * Given a query which will return two non-null IDs in the first two columns as results, this
1224     * method will put two entries into the given result map for each pair of different IDs, one
1225     * keyed by each ID.
1226     */
1227    private void findIdPairs(SQLiteDatabase db, String query, Multimap<Long, Long> results) {
1228        Cursor cursor = db.rawQuery(query, null);
1229        try {
1230            cursor.moveToPosition(-1);
1231            while (cursor.moveToNext()) {
1232                long idA = cursor.getLong(0);
1233                long idB = cursor.getLong(1);
1234                if (idA != idB) {
1235                    results.put(idA, idB);
1236                    results.put(idB, idA);
1237                }
1238            }
1239        } finally {
1240            cursor.close();
1241        }
1242    }
1243
1244    /**
1245     * Creates a new Contact for a given set of the raw contacts of {@code rawContactIds} if the
1246     * given contactId is null. Otherwise, regroup them into contact with {@code contactId}.
1247     */
1248    private void createContactForRawContacts(SQLiteDatabase db, TransactionContext txContext,
1249            Set<Long> rawContactIds, Long contactId) {
1250        if (rawContactIds.isEmpty()) {
1251            // No raw contact id is provided.
1252            return;
1253        }
1254
1255        // If contactId is not provided, generates a new one.
1256        if (contactId == null) {
1257            mSelectionArgs1[0]= String.valueOf(rawContactIds.iterator().next());
1258            computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
1259                    mContactInsert);
1260            contactId = mContactInsert.executeInsert();
1261        }
1262        for (Long rawContactId : rawContactIds) {
1263            // Regrouped contacts should automatically be unpinned.
1264            unpinRawContact(rawContactId);
1265            setContactIdAndMarkAggregated(rawContactId, contactId);
1266            setPresenceContactId(rawContactId, contactId);
1267        }
1268        updateAggregateData(txContext, contactId);
1269    }
1270
1271    private static class RawContactIdQuery {
1272        public static final String TABLE = Tables.RAW_CONTACTS;
1273        public static final String[] COLUMNS = { RawContacts._ID };
1274        public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
1275        public static final int RAW_CONTACT_ID = 0;
1276    }
1277
1278    /**
1279     * Ensures that automatic aggregation rules are followed after a contact
1280     * becomes visible or invisible. Specifically, consider this case: there are
1281     * three contacts named Foo. Two of them come from account A1 and one comes
1282     * from account A2. The aggregation rules say that in this case none of the
1283     * three Foo's should be aggregated: two of them are in the same account, so
1284     * they don't get aggregated; the third has two affinities, so it does not
1285     * join either of them.
1286     * <p>
1287     * Consider what happens if one of the "Foo"s from account A1 becomes
1288     * invisible. Nothing stands in the way of aggregating the other two
1289     * anymore, so they should get joined.
1290     * <p>
1291     * What if the invisible "Foo" becomes visible after that? We should split the
1292     * aggregate between the other two.
1293     */
1294    public void updateAggregationAfterVisibilityChange(long contactId) {
1295        SQLiteDatabase db = mDbHelper.getWritableDatabase();
1296        boolean visible = mDbHelper.isContactInDefaultDirectory(db, contactId);
1297        if (visible) {
1298            markContactForAggregation(db, contactId);
1299        } else {
1300            // Find all contacts that _could be_ aggregated with this one and
1301            // rerun aggregation for all of them
1302            mSelectionArgs1[0] = String.valueOf(contactId);
1303            Cursor cursor = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
1304                    RawContactIdQuery.SELECTION, mSelectionArgs1, null, null, null);
1305            try {
1306                while (cursor.moveToNext()) {
1307                    long rawContactId = cursor.getLong(RawContactIdQuery.RAW_CONTACT_ID);
1308                    mMatcher.clear();
1309
1310                    updateMatchScoresBasedOnIdentityMatch(db, rawContactId, mMatcher);
1311                    updateMatchScoresBasedOnNameMatches(db, rawContactId, mMatcher);
1312                    List<MatchScore> bestMatches =
1313                            mMatcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
1314                    for (MatchScore matchScore : bestMatches) {
1315                        markContactForAggregation(db, matchScore.getContactId());
1316                    }
1317
1318                    mMatcher.clear();
1319                    updateMatchScoresBasedOnEmailMatches(db, rawContactId, mMatcher);
1320                    updateMatchScoresBasedOnPhoneMatches(db, rawContactId, mMatcher);
1321                    bestMatches =
1322                            mMatcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
1323                    for (MatchScore matchScore : bestMatches) {
1324                        markContactForAggregation(db, matchScore.getContactId());
1325                    }
1326                }
1327            } finally {
1328                cursor.close();
1329            }
1330        }
1331    }
1332
1333    /**
1334     * Updates the contact ID for the specified contact.
1335     */
1336    protected void setContactId(long rawContactId, long contactId) {
1337        mContactIdUpdate.bindLong(1, contactId);
1338        mContactIdUpdate.bindLong(2, rawContactId);
1339        mContactIdUpdate.execute();
1340    }
1341
1342    /**
1343     * Marks the specified raw contact ID as aggregated
1344     */
1345    private void markAggregated(long rawContactId) {
1346        mMarkAggregatedUpdate.bindLong(1, rawContactId);
1347        mMarkAggregatedUpdate.execute();
1348    }
1349
1350    /**
1351     * Updates the contact ID for the specified contact and marks the raw contact as aggregated.
1352     */
1353    private void setContactIdAndMarkAggregated(long rawContactId, long contactId) {
1354        mContactIdAndMarkAggregatedUpdate.bindLong(1, contactId);
1355        mContactIdAndMarkAggregatedUpdate.bindLong(2, rawContactId);
1356        mContactIdAndMarkAggregatedUpdate.execute();
1357    }
1358
1359    private void setPresenceContactId(long rawContactId, long contactId) {
1360        mPresenceContactIdUpdate.bindLong(1, contactId);
1361        mPresenceContactIdUpdate.bindLong(2, rawContactId);
1362        mPresenceContactIdUpdate.execute();
1363    }
1364
1365    private void unpinRawContact(long rawContactId) {
1366        mResetPinnedForRawContact.bindLong(1, rawContactId);
1367        mResetPinnedForRawContact.execute();
1368    }
1369
1370    interface AggregateExceptionPrefetchQuery {
1371        String TABLE = Tables.AGGREGATION_EXCEPTIONS;
1372
1373        String[] COLUMNS = {
1374            AggregationExceptions.RAW_CONTACT_ID1,
1375            AggregationExceptions.RAW_CONTACT_ID2,
1376        };
1377
1378        int RAW_CONTACT_ID1 = 0;
1379        int RAW_CONTACT_ID2 = 1;
1380    }
1381
1382    // A set of raw contact IDs for which there are aggregation exceptions
1383    private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>();
1384    private boolean mAggregationExceptionIdsValid;
1385
1386    public void invalidateAggregationExceptionCache() {
1387        mAggregationExceptionIdsValid = false;
1388    }
1389
1390    /**
1391     * Finds all raw contact IDs for which there are aggregation exceptions. The list of
1392     * ids is used as an optimization in aggregation: there is no point to run a query against
1393     * the agg_exceptions table if it is known that there are no records there for a given
1394     * raw contact ID.
1395     */
1396    private void prefetchAggregationExceptionIds(SQLiteDatabase db) {
1397        mAggregationExceptionIds.clear();
1398        final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE,
1399                AggregateExceptionPrefetchQuery.COLUMNS,
1400                null, null, null, null, null);
1401
1402        try {
1403            while (c.moveToNext()) {
1404                long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1);
1405                long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2);
1406                mAggregationExceptionIds.add(rawContactId1);
1407                mAggregationExceptionIds.add(rawContactId2);
1408            }
1409        } finally {
1410            c.close();
1411        }
1412
1413        mAggregationExceptionIdsValid = true;
1414    }
1415
1416    interface AggregateExceptionQuery {
1417        String TABLE = Tables.AGGREGATION_EXCEPTIONS
1418            + " JOIN raw_contacts raw_contacts1 "
1419                    + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) "
1420            + " JOIN raw_contacts raw_contacts2 "
1421                    + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) ";
1422
1423        String[] COLUMNS = {
1424            AggregationExceptions.TYPE,
1425            AggregationExceptions.RAW_CONTACT_ID1,
1426            "raw_contacts1." + RawContacts.CONTACT_ID,
1427            "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED,
1428            "raw_contacts2." + RawContacts.CONTACT_ID,
1429            "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED,
1430        };
1431
1432        int TYPE = 0;
1433        int RAW_CONTACT_ID1 = 1;
1434        int CONTACT_ID1 = 2;
1435        int AGGREGATION_NEEDED_1 = 3;
1436        int CONTACT_ID2 = 4;
1437        int AGGREGATION_NEEDED_2 = 5;
1438    }
1439
1440    /**
1441     * Computes match scores based on exceptions entered by the user: always match and never match.
1442     * Returns the aggregate contact with the always match exception if any.
1443     */
1444    private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId,
1445            ContactMatcher matcher) {
1446        if (!mAggregationExceptionIdsValid) {
1447            prefetchAggregationExceptionIds(db);
1448        }
1449
1450        // If there are no aggregation exceptions involving this raw contact, there is no need to
1451        // run a query and we can just return -1, which stands for "nothing found"
1452        if (!mAggregationExceptionIds.contains(rawContactId)) {
1453            return -1;
1454        }
1455
1456        final Cursor c = db.query(AggregateExceptionQuery.TABLE,
1457                AggregateExceptionQuery.COLUMNS,
1458                AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId
1459                        + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId,
1460                null, null, null, null);
1461
1462        try {
1463            while (c.moveToNext()) {
1464                int type = c.getInt(AggregateExceptionQuery.TYPE);
1465                long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1);
1466                long contactId = -1;
1467                if (rawContactId == rawContactId1) {
1468                    if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_2) == 0
1469                            && !c.isNull(AggregateExceptionQuery.CONTACT_ID2)) {
1470                        contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2);
1471                    }
1472                } else {
1473                    if (c.getInt(AggregateExceptionQuery.AGGREGATION_NEEDED_1) == 0
1474                            && !c.isNull(AggregateExceptionQuery.CONTACT_ID1)) {
1475                        contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1);
1476                    }
1477                }
1478                if (contactId != -1) {
1479                    if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) {
1480                        matcher.keepIn(contactId);
1481                    } else {
1482                        matcher.keepOut(contactId);
1483                    }
1484                }
1485            }
1486        } finally {
1487            c.close();
1488        }
1489
1490        return matcher.pickBestMatch(ContactMatcher.MAX_SCORE, true);
1491    }
1492
1493    /**
1494     * Picks the best matching contact based on matches between data elements.  It considers
1495     * name match to be primary and phone, email etc matches to be secondary.  A good primary
1496     * match triggers aggregation, while a good secondary match only triggers aggregation in
1497     * the absence of a strong primary mismatch.
1498     * <p>
1499     * Consider these examples:
1500     * <p>
1501     * John Doe with phone number 111-111-1111 and Jon Doe with phone number 111-111-1111 should
1502     * be aggregated (same number, similar names).
1503     * <p>
1504     * John Doe with phone number 111-111-1111 and Deborah Doe with phone number 111-111-1111 should
1505     * not be aggregated (same number, different names).
1506     */
1507    private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId,
1508            MatchCandidateList candidates, ContactMatcher matcher) {
1509
1510        // Find good matches based on name alone
1511        long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, matcher);
1512        if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) {
1513            // We found multiple matches on the name - do not aggregate because of the ambiguity
1514            return -1;
1515        } else if (bestMatch == -1) {
1516            // We haven't found a good match on name, see if we have any matches on phone, email etc
1517            bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher);
1518            if (bestMatch == ContactMatcher.MULTIPLE_MATCHES) {
1519                return -1;
1520            }
1521        }
1522
1523        return bestMatch;
1524    }
1525
1526
1527    /**
1528     * Picks the best matching contact based on secondary data matches.  The method loads
1529     * structured names for all candidate contacts and recomputes match scores using approximate
1530     * matching.
1531     */
1532    private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db,
1533            long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
1534        List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates(
1535                ContactMatcher.SCORE_THRESHOLD_PRIMARY);
1536        if (secondaryContactIds == null || secondaryContactIds.size() > SECONDARY_HIT_LIMIT) {
1537            return -1;
1538        }
1539
1540        loadNameMatchCandidates(db, rawContactId, candidates, true);
1541
1542        mSb.setLength(0);
1543        mSb.append(RawContacts.CONTACT_ID).append(" IN (");
1544        for (int i = 0; i < secondaryContactIds.size(); i++) {
1545            if (i != 0) {
1546                mSb.append(',');
1547            }
1548            mSb.append(secondaryContactIds.get(i));
1549        }
1550
1551        // We only want to compare structured names to structured names
1552        // at this stage, we need to ignore all other sources of name lookup data.
1553        mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL);
1554
1555        matchAllCandidates(db, mSb.toString(), candidates, matcher,
1556                ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null);
1557
1558        return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY, false);
1559    }
1560
1561    private interface NameLookupQuery {
1562        String TABLE = Tables.NAME_LOOKUP;
1563
1564        String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?";
1565        String SELECTION_STRUCTURED_NAME_BASED =
1566                SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL;
1567
1568        String[] COLUMNS = new String[] {
1569                NameLookupColumns.NORMALIZED_NAME,
1570                NameLookupColumns.NAME_TYPE
1571        };
1572
1573        int NORMALIZED_NAME = 0;
1574        int NAME_TYPE = 1;
1575    }
1576
1577    private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId,
1578            MatchCandidateList candidates, boolean structuredNameBased) {
1579        candidates.clear();
1580        mSelectionArgs1[0] = String.valueOf(rawContactId);
1581        Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
1582                structuredNameBased
1583                        ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED
1584                        : NameLookupQuery.SELECTION,
1585                mSelectionArgs1, null, null, null);
1586        try {
1587            while (c.moveToNext()) {
1588                String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME);
1589                int type = c.getInt(NameLookupQuery.NAME_TYPE);
1590                candidates.add(normalizedName, type);
1591            }
1592        } finally {
1593            c.close();
1594        }
1595    }
1596
1597    /**
1598     * Computes scores for contacts that have matching data rows.
1599     */
1600    private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
1601            ContactMatcher matcher) {
1602
1603        updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
1604        updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
1605        long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY, false);
1606        if (bestMatch != -1) {
1607            return bestMatch;
1608        }
1609
1610        updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
1611        updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
1612
1613        return -1;
1614    }
1615
1616    private interface IdentityLookupMatchQuery {
1617        final String TABLE = Tables.DATA + " dataA"
1618                + " JOIN " + Tables.DATA + " dataB" +
1619                " ON (dataA." + Identity.NAMESPACE + "=dataB." + Identity.NAMESPACE +
1620                " AND dataA." + Identity.IDENTITY + "=dataB." + Identity.IDENTITY + ")"
1621                + " JOIN " + Tables.RAW_CONTACTS +
1622                " ON (dataB." + Data.RAW_CONTACT_ID + " = "
1623                + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1624
1625        final String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
1626                + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
1627                + " AND dataA." + Identity.NAMESPACE + " NOT NULL"
1628                + " AND dataA." + Identity.IDENTITY + " NOT NULL"
1629                + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
1630                + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
1631                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1632
1633        final String[] COLUMNS = new String[] {
1634            RawContacts.CONTACT_ID
1635        };
1636
1637        int CONTACT_ID = 0;
1638    }
1639
1640    /**
1641     * Finds contacts with exact identity matches to the the specified raw contact.
1642     */
1643    private void updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId,
1644            ContactMatcher matcher) {
1645        mSelectionArgs2[0] = String.valueOf(rawContactId);
1646        mSelectionArgs2[1] = String.valueOf(mMimeTypeIdIdentity);
1647        Cursor c = db.query(IdentityLookupMatchQuery.TABLE, IdentityLookupMatchQuery.COLUMNS,
1648                IdentityLookupMatchQuery.SELECTION,
1649                mSelectionArgs2, RawContacts.CONTACT_ID, null, null);
1650        try {
1651            while (c.moveToNext()) {
1652                final long contactId = c.getLong(IdentityLookupMatchQuery.CONTACT_ID);
1653                matcher.matchIdentity(contactId);
1654            }
1655        } finally {
1656            c.close();
1657        }
1658
1659    }
1660
1661    private interface NameLookupMatchQuery {
1662        String TABLE = Tables.NAME_LOOKUP + " nameA"
1663                + " JOIN " + Tables.NAME_LOOKUP + " nameB" +
1664                " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "="
1665                        + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")"
1666                + " JOIN " + Tables.RAW_CONTACTS +
1667                " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = "
1668                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1669
1670        String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?"
1671                + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
1672                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1673
1674        String[] COLUMNS = new String[] {
1675            RawContacts.CONTACT_ID,
1676            "nameA." + NameLookupColumns.NORMALIZED_NAME,
1677            "nameA." + NameLookupColumns.NAME_TYPE,
1678            "nameB." + NameLookupColumns.NAME_TYPE,
1679        };
1680
1681        int CONTACT_ID = 0;
1682        int NAME = 1;
1683        int NAME_TYPE_A = 2;
1684        int NAME_TYPE_B = 3;
1685    }
1686
1687    /**
1688     * Finds contacts with names matching the name of the specified raw contact.
1689     */
1690    private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
1691            ContactMatcher matcher) {
1692        mSelectionArgs1[0] = String.valueOf(rawContactId);
1693        Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS,
1694                NameLookupMatchQuery.SELECTION,
1695                mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING);
1696        try {
1697            while (c.moveToNext()) {
1698                long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID);
1699                String name = c.getString(NameLookupMatchQuery.NAME);
1700                int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A);
1701                int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B);
1702                matcher.matchName(contactId, nameTypeA, name,
1703                        nameTypeB, name, ContactMatcher.MATCHING_ALGORITHM_EXACT);
1704                if (nameTypeA == NameLookupType.NICKNAME &&
1705                        nameTypeB == NameLookupType.NICKNAME) {
1706                    matcher.updateScoreWithNicknameMatch(contactId);
1707                }
1708            }
1709        } finally {
1710            c.close();
1711        }
1712    }
1713
1714    private interface NameLookupMatchQueryWithParameter {
1715        String TABLE = Tables.NAME_LOOKUP
1716                + " JOIN " + Tables.RAW_CONTACTS +
1717                " ON (" + NameLookupColumns.RAW_CONTACT_ID + " = "
1718                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1719
1720        String[] COLUMNS = new String[] {
1721            RawContacts.CONTACT_ID,
1722            NameLookupColumns.NORMALIZED_NAME,
1723            NameLookupColumns.NAME_TYPE,
1724        };
1725
1726        int CONTACT_ID = 0;
1727        int NAME = 1;
1728        int NAME_TYPE = 2;
1729    }
1730
1731    private final class NameLookupSelectionBuilder extends NameLookupBuilder {
1732
1733        private final MatchCandidateList mNameLookupCandidates;
1734
1735        private StringBuilder mSelection = new StringBuilder(
1736                NameLookupColumns.NORMALIZED_NAME + " IN(");
1737
1738
1739        public NameLookupSelectionBuilder(NameSplitter splitter, MatchCandidateList candidates) {
1740            super(splitter);
1741            this.mNameLookupCandidates = candidates;
1742        }
1743
1744        @Override
1745        protected String[] getCommonNicknameClusters(String normalizedName) {
1746            return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
1747        }
1748
1749        @Override
1750        protected void insertNameLookup(
1751                long rawContactId, long dataId, int lookupType, String string) {
1752            mNameLookupCandidates.add(string, lookupType);
1753            DatabaseUtils.appendEscapedSQLString(mSelection, string);
1754            mSelection.append(',');
1755        }
1756
1757        public boolean isEmpty() {
1758            return mNameLookupCandidates.isEmpty();
1759        }
1760
1761        public String getSelection() {
1762            mSelection.setLength(mSelection.length() - 1);      // Strip last comma
1763            mSelection.append(')');
1764            return mSelection.toString();
1765        }
1766
1767        public int getLookupType(String name) {
1768            for (int i = 0; i < mNameLookupCandidates.mCount; i++) {
1769                if (mNameLookupCandidates.mList.get(i).mName.equals(name)) {
1770                    return mNameLookupCandidates.mList.get(i).mLookupType;
1771                }
1772            }
1773            throw new IllegalStateException();
1774        }
1775    }
1776
1777    /**
1778     * Finds contacts with names matching the specified name.
1779     */
1780    private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query,
1781            MatchCandidateList candidates, ContactMatcher matcher) {
1782        candidates.clear();
1783        NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder(
1784                mNameSplitter, candidates);
1785        builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED);
1786        if (builder.isEmpty()) {
1787            return;
1788        }
1789
1790        Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE,
1791                NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null,
1792                null, PRIMARY_HIT_LIMIT_STRING);
1793        try {
1794            while (c.moveToNext()) {
1795                long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID);
1796                String name = c.getString(NameLookupMatchQueryWithParameter.NAME);
1797                int nameTypeA = builder.getLookupType(name);
1798                int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE);
1799                matcher.matchName(contactId, nameTypeA, name, nameTypeB, name,
1800                        ContactMatcher.MATCHING_ALGORITHM_EXACT);
1801                if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) {
1802                    matcher.updateScoreWithNicknameMatch(contactId);
1803                }
1804            }
1805        } finally {
1806            c.close();
1807        }
1808    }
1809
1810    private interface EmailLookupQuery {
1811        String TABLE = Tables.DATA + " dataA"
1812                + " JOIN " + Tables.DATA + " dataB" +
1813                " ON lower(" + "dataA." + Email.DATA + ")=lower(dataB." + Email.DATA + ")"
1814                + " JOIN " + Tables.RAW_CONTACTS +
1815                " ON (dataB." + Data.RAW_CONTACT_ID + " = "
1816                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1817
1818        String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
1819                + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
1820                + " AND dataA." + Email.DATA + " NOT NULL"
1821                + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
1822                + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
1823                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1824
1825        String[] COLUMNS = new String[] {
1826            RawContacts.CONTACT_ID
1827        };
1828
1829        int CONTACT_ID = 0;
1830    }
1831
1832    private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
1833            ContactMatcher matcher) {
1834        mSelectionArgs2[0] = String.valueOf(rawContactId);
1835        mSelectionArgs2[1] = String.valueOf(mMimeTypeIdEmail);
1836        Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
1837                EmailLookupQuery.SELECTION,
1838                mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
1839        try {
1840            while (c.moveToNext()) {
1841                long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
1842                matcher.updateScoreWithEmailMatch(contactId);
1843            }
1844        } finally {
1845            c.close();
1846        }
1847    }
1848
1849    private interface PhoneLookupQuery {
1850        String TABLE = Tables.PHONE_LOOKUP + " phoneA"
1851                + " JOIN " + Tables.DATA + " dataA"
1852                + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
1853                + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
1854                + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
1855                        + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
1856                + " JOIN " + Tables.DATA + " dataB"
1857                + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
1858                + " JOIN " + Tables.RAW_CONTACTS
1859                + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
1860                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1861
1862        String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
1863                + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
1864                        + "dataB." + Phone.NUMBER + ",?)"
1865                + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
1866                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1867
1868        String[] COLUMNS = new String[] {
1869            RawContacts.CONTACT_ID
1870        };
1871
1872        int CONTACT_ID = 0;
1873    }
1874
1875    private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
1876            ContactMatcher matcher) {
1877        mSelectionArgs2[0] = String.valueOf(rawContactId);
1878        mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter();
1879        Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
1880                PhoneLookupQuery.SELECTION,
1881                mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
1882        try {
1883            while (c.moveToNext()) {
1884                long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
1885                matcher.updateScoreWithPhoneNumberMatch(contactId);
1886            }
1887        } finally {
1888            c.close();
1889        }
1890    }
1891
1892    /**
1893     * Loads name lookup rows for approximate name matching and updates match scores based on that
1894     * data.
1895     */
1896    private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
1897            ContactMatcher matcher) {
1898        HashSet<String> firstLetters = new HashSet<String>();
1899        for (int i = 0; i < candidates.mCount; i++) {
1900            final NameMatchCandidate candidate = candidates.mList.get(i);
1901            if (candidate.mName.length() >= 2) {
1902                String firstLetter = candidate.mName.substring(0, 2);
1903                if (!firstLetters.contains(firstLetter)) {
1904                    firstLetters.add(firstLetter);
1905                    final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '"
1906                            + firstLetter + "*') AND "
1907                            + "(" + NameLookupColumns.NAME_TYPE + " IN("
1908                                    + NameLookupType.NAME_COLLATION_KEY + ","
1909                                    + NameLookupType.EMAIL_BASED_NICKNAME + ","
1910                                    + NameLookupType.NICKNAME + ")) AND "
1911                            + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1912                    matchAllCandidates(db, selection, candidates, matcher,
1913                            ContactMatcher.MATCHING_ALGORITHM_APPROXIMATE,
1914                            String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT));
1915                }
1916            }
1917        }
1918    }
1919
1920    private interface ContactNameLookupQuery {
1921        String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
1922
1923        String[] COLUMNS = new String[] {
1924                RawContacts.CONTACT_ID,
1925                NameLookupColumns.NORMALIZED_NAME,
1926                NameLookupColumns.NAME_TYPE
1927        };
1928
1929        int CONTACT_ID = 0;
1930        int NORMALIZED_NAME = 1;
1931        int NAME_TYPE = 2;
1932    }
1933
1934    /**
1935     * Loads all candidate rows from the name lookup table and updates match scores based
1936     * on that data.
1937     */
1938    private void matchAllCandidates(SQLiteDatabase db, String selection,
1939            MatchCandidateList candidates, ContactMatcher matcher, int algorithm, String limit) {
1940        final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
1941                selection, null, null, null, null, limit);
1942
1943        try {
1944            while (c.moveToNext()) {
1945                Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
1946                String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
1947                int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
1948
1949                // Note the N^2 complexity of the following fragment. This is not a huge concern
1950                // since the number of candidates is very small and in general secondary hits
1951                // in the absence of primary hits are rare.
1952                for (int i = 0; i < candidates.mCount; i++) {
1953                    NameMatchCandidate candidate = candidates.mList.get(i);
1954                    matcher.matchName(contactId, candidate.mLookupType, candidate.mName,
1955                            nameType, name, algorithm);
1956                }
1957            }
1958        } finally {
1959            c.close();
1960        }
1961    }
1962
1963    private interface RawContactsQuery {
1964        String SQL_FORMAT =
1965                "SELECT "
1966                        + RawContactsColumns.CONCRETE_ID + ","
1967                        + RawContactsColumns.DISPLAY_NAME + ","
1968                        + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
1969                        + AccountsColumns.CONCRETE_ACCOUNT_TYPE + ","
1970                        + AccountsColumns.CONCRETE_ACCOUNT_NAME + ","
1971                        + AccountsColumns.CONCRETE_DATA_SET + ","
1972                        + RawContacts.SOURCE_ID + ","
1973                        + RawContacts.CUSTOM_RINGTONE + ","
1974                        + RawContacts.SEND_TO_VOICEMAIL + ","
1975                        + RawContacts.LAST_TIME_CONTACTED + ","
1976                        + RawContacts.TIMES_CONTACTED + ","
1977                        + RawContacts.STARRED + ","
1978                        + RawContacts.PINNED + ","
1979                        + RawContacts.NAME_VERIFIED + ","
1980                        + DataColumns.CONCRETE_ID + ","
1981                        + DataColumns.CONCRETE_MIMETYPE_ID + ","
1982                        + Data.IS_SUPER_PRIMARY + ","
1983                        + Photo.PHOTO_FILE_ID +
1984                " FROM " + Tables.RAW_CONTACTS +
1985                " JOIN " + Tables.ACCOUNTS + " ON ("
1986                    + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
1987                    + ")" +
1988                " LEFT OUTER JOIN " + Tables.DATA +
1989                " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
1990                        + " AND ((" + DataColumns.MIMETYPE_ID + "=%d"
1991                                + " AND " + Photo.PHOTO + " NOT NULL)"
1992                        + " OR (" + DataColumns.MIMETYPE_ID + "=%d"
1993                                + " AND " + Phone.NUMBER + " NOT NULL)))";
1994
1995        String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT +
1996                " WHERE " + RawContactsColumns.CONCRETE_ID + "=?";
1997
1998        String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT +
1999                " WHERE " + RawContacts.CONTACT_ID + "=?"
2000                + " AND " + RawContacts.DELETED + "=0";
2001
2002        int RAW_CONTACT_ID = 0;
2003        int DISPLAY_NAME = 1;
2004        int DISPLAY_NAME_SOURCE = 2;
2005        int ACCOUNT_TYPE = 3;
2006        int ACCOUNT_NAME = 4;
2007        int DATA_SET = 5;
2008        int SOURCE_ID = 6;
2009        int CUSTOM_RINGTONE = 7;
2010        int SEND_TO_VOICEMAIL = 8;
2011        int LAST_TIME_CONTACTED = 9;
2012        int TIMES_CONTACTED = 10;
2013        int STARRED = 11;
2014        int PINNED = 12;
2015        int NAME_VERIFIED = 13;
2016        int DATA_ID = 14;
2017        int MIMETYPE_ID = 15;
2018        int IS_SUPER_PRIMARY = 16;
2019        int PHOTO_FILE_ID = 17;
2020    }
2021
2022    private interface ContactReplaceSqlStatement {
2023        String UPDATE_SQL =
2024                "UPDATE " + Tables.CONTACTS +
2025                " SET "
2026                        + Contacts.NAME_RAW_CONTACT_ID + "=?, "
2027                        + Contacts.PHOTO_ID + "=?, "
2028                        + Contacts.PHOTO_FILE_ID + "=?, "
2029                        + Contacts.SEND_TO_VOICEMAIL + "=?, "
2030                        + Contacts.CUSTOM_RINGTONE + "=?, "
2031                        + Contacts.LAST_TIME_CONTACTED + "=?, "
2032                        + Contacts.TIMES_CONTACTED + "=?, "
2033                        + Contacts.STARRED + "=?, "
2034                        + Contacts.PINNED + "=?, "
2035                        + Contacts.HAS_PHONE_NUMBER + "=?, "
2036                        + Contacts.LOOKUP_KEY + "=?, "
2037                        + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=? " +
2038                " WHERE " + Contacts._ID + "=?";
2039
2040        String INSERT_SQL =
2041                "INSERT INTO " + Tables.CONTACTS + " ("
2042                        + Contacts.NAME_RAW_CONTACT_ID + ", "
2043                        + Contacts.PHOTO_ID + ", "
2044                        + Contacts.PHOTO_FILE_ID + ", "
2045                        + Contacts.SEND_TO_VOICEMAIL + ", "
2046                        + Contacts.CUSTOM_RINGTONE + ", "
2047                        + Contacts.LAST_TIME_CONTACTED + ", "
2048                        + Contacts.TIMES_CONTACTED + ", "
2049                        + Contacts.STARRED + ", "
2050                        + Contacts.PINNED + ", "
2051                        + Contacts.HAS_PHONE_NUMBER + ", "
2052                        + Contacts.LOOKUP_KEY + ", "
2053                        + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
2054                        + ") " +
2055                " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";
2056
2057        int NAME_RAW_CONTACT_ID = 1;
2058        int PHOTO_ID = 2;
2059        int PHOTO_FILE_ID = 3;
2060        int SEND_TO_VOICEMAIL = 4;
2061        int CUSTOM_RINGTONE = 5;
2062        int LAST_TIME_CONTACTED = 6;
2063        int TIMES_CONTACTED = 7;
2064        int STARRED = 8;
2065        int PINNED = 9;
2066        int HAS_PHONE_NUMBER = 10;
2067        int LOOKUP_KEY = 11;
2068        int CONTACT_LAST_UPDATED_TIMESTAMP = 12;
2069        int CONTACT_ID = 13;
2070    }
2071
2072    /**
2073     * Computes aggregate-level data for the specified aggregate contact ID.
2074     */
2075    private void computeAggregateData(SQLiteDatabase db, long contactId,
2076            SQLiteStatement statement) {
2077        mSelectionArgs1[0] = String.valueOf(contactId);
2078        computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement);
2079    }
2080
2081    /**
2082     * Indicates whether the given photo entry and priority gives this photo a higher overall
2083     * priority than the current best photo entry and priority.
2084     */
2085    private boolean hasHigherPhotoPriority(PhotoEntry photoEntry, int priority,
2086            PhotoEntry bestPhotoEntry, int bestPriority) {
2087        int photoComparison = photoEntry.compareTo(bestPhotoEntry);
2088        return photoComparison < 0 || photoComparison == 0 && priority > bestPriority;
2089    }
2090
2091    /**
2092     * Computes aggregate-level data from constituent raw contacts.
2093     */
2094    private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs,
2095            SQLiteStatement statement) {
2096        long currentRawContactId = -1;
2097        long bestPhotoId = -1;
2098        long bestPhotoFileId = 0;
2099        PhotoEntry bestPhotoEntry = null;
2100        boolean foundSuperPrimaryPhoto = false;
2101        int photoPriority = -1;
2102        int totalRowCount = 0;
2103        int contactSendToVoicemail = 0;
2104        String contactCustomRingtone = null;
2105        long contactLastTimeContacted = 0;
2106        int contactTimesContacted = 0;
2107        int contactStarred = 0;
2108        int contactPinned = Integer.MAX_VALUE;
2109        int hasPhoneNumber = 0;
2110        StringBuilder lookupKey = new StringBuilder();
2111
2112        mDisplayNameCandidate.clear();
2113
2114        Cursor c = db.rawQuery(sql, sqlArgs);
2115        try {
2116            while (c.moveToNext()) {
2117                long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
2118                if (rawContactId != currentRawContactId) {
2119                    currentRawContactId = rawContactId;
2120                    totalRowCount++;
2121
2122                    // Assemble sub-account.
2123                    String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
2124                    String dataSet = c.getString(RawContactsQuery.DATA_SET);
2125                    String accountWithDataSet = (!TextUtils.isEmpty(dataSet))
2126                            ? accountType + "/" + dataSet
2127                            : accountType;
2128
2129                    // Display name
2130                    String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
2131                    int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE);
2132                    int nameVerified = c.getInt(RawContactsQuery.NAME_VERIFIED);
2133                    processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
2134                            mContactsProvider.isWritableAccountWithDataSet(accountWithDataSet),
2135                            nameVerified != 0);
2136
2137                    // Contact options
2138                    if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
2139                        boolean sendToVoicemail =
2140                                (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0);
2141                        if (sendToVoicemail) {
2142                            contactSendToVoicemail++;
2143                        }
2144                    }
2145
2146                    if (contactCustomRingtone == null
2147                            && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) {
2148                        contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE);
2149                    }
2150
2151                    long lastTimeContacted = c.getLong(RawContactsQuery.LAST_TIME_CONTACTED);
2152                    if (lastTimeContacted > contactLastTimeContacted) {
2153                        contactLastTimeContacted = lastTimeContacted;
2154                    }
2155
2156                    int timesContacted = c.getInt(RawContactsQuery.TIMES_CONTACTED);
2157                    if (timesContacted > contactTimesContacted) {
2158                        contactTimesContacted = timesContacted;
2159                    }
2160
2161                    if (c.getInt(RawContactsQuery.STARRED) != 0) {
2162                        contactStarred = 1;
2163                    }
2164
2165                    // contactPinned should be the lowest value of its constituent raw contacts,
2166                    // excluding negative integers
2167                    final int rawContactPinned = c.getInt(RawContactsQuery.PINNED);
2168                    if (rawContactPinned > PinnedPositions.UNPINNED) {
2169                        contactPinned = Math.min(contactPinned, rawContactPinned);
2170                    }
2171
2172                    appendLookupKey(
2173                            lookupKey,
2174                            accountWithDataSet,
2175                            c.getString(RawContactsQuery.ACCOUNT_NAME),
2176                            rawContactId,
2177                            c.getString(RawContactsQuery.SOURCE_ID),
2178                            displayName);
2179                }
2180
2181                if (!c.isNull(RawContactsQuery.DATA_ID)) {
2182                    long dataId = c.getLong(RawContactsQuery.DATA_ID);
2183                    long photoFileId = c.getLong(RawContactsQuery.PHOTO_FILE_ID);
2184                    int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
2185                    boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0;
2186                    if (mimetypeId == mMimeTypeIdPhoto) {
2187                        if (!foundSuperPrimaryPhoto) {
2188                            // Lookup the metadata for the photo, if available.  Note that data set
2189                            // does not come into play here, since accounts are looked up in the
2190                            // account manager in the priority resolver.
2191                            PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
2192                            String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
2193                            int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
2194                            if (superPrimary || hasHigherPhotoPriority(
2195                                    photoEntry, priority, bestPhotoEntry, photoPriority)) {
2196                                bestPhotoEntry = photoEntry;
2197                                photoPriority = priority;
2198                                bestPhotoId = dataId;
2199                                bestPhotoFileId = photoFileId;
2200                                foundSuperPrimaryPhoto |= superPrimary;
2201                            }
2202                        }
2203                    } else if (mimetypeId == mMimeTypeIdPhone) {
2204                        hasPhoneNumber = 1;
2205                    }
2206                }
2207            }
2208        } finally {
2209            c.close();
2210        }
2211
2212        if (contactPinned == Integer.MAX_VALUE) {
2213            contactPinned = PinnedPositions.UNPINNED;
2214        }
2215
2216        statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID,
2217                mDisplayNameCandidate.rawContactId);
2218
2219        if (bestPhotoId != -1) {
2220            statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId);
2221        } else {
2222            statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID);
2223        }
2224
2225        if (bestPhotoFileId != 0) {
2226            statement.bindLong(ContactReplaceSqlStatement.PHOTO_FILE_ID, bestPhotoFileId);
2227        } else {
2228            statement.bindNull(ContactReplaceSqlStatement.PHOTO_FILE_ID);
2229        }
2230
2231        statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL,
2232                totalRowCount == contactSendToVoicemail ? 1 : 0);
2233        DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
2234                contactCustomRingtone);
2235        statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED,
2236                contactLastTimeContacted);
2237        statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED,
2238                contactTimesContacted);
2239        statement.bindLong(ContactReplaceSqlStatement.STARRED,
2240                contactStarred);
2241        statement.bindLong(ContactReplaceSqlStatement.PINNED,
2242                contactPinned);
2243        statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER,
2244                hasPhoneNumber);
2245        statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
2246                Uri.encode(lookupKey.toString()));
2247        statement.bindLong(ContactReplaceSqlStatement.CONTACT_LAST_UPDATED_TIMESTAMP,
2248                Clock.getInstance().currentTimeMillis());
2249    }
2250
2251    /**
2252     * Builds a lookup key using the given data.
2253     */
2254    protected void appendLookupKey(StringBuilder sb, String accountTypeWithDataSet,
2255            String accountName, long rawContactId, String sourceId, String displayName) {
2256        ContactLookupKey.appendToLookupKey(sb, accountTypeWithDataSet, accountName, rawContactId,
2257                sourceId, displayName);
2258    }
2259
2260    /**
2261     * Uses the supplied values to determine if they represent a "better" display name
2262     * for the aggregate contact currently evaluated.  If so, it updates
2263     * {@link #mDisplayNameCandidate} with the new values.
2264     */
2265    private void processDisplayNameCandidate(long rawContactId, String displayName,
2266            int displayNameSource, boolean writableAccount, boolean verified) {
2267
2268        boolean replace = false;
2269        if (mDisplayNameCandidate.rawContactId == -1) {
2270            // No previous values available
2271            replace = true;
2272        } else if (!TextUtils.isEmpty(displayName)) {
2273            if (!mDisplayNameCandidate.verified && verified) {
2274                // A verified name is better than any other name
2275                replace = true;
2276            } else if (mDisplayNameCandidate.verified == verified) {
2277                if (mDisplayNameCandidate.displayNameSource < displayNameSource) {
2278                    // New values come from an superior source, e.g. structured name vs phone number
2279                    replace = true;
2280                } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) {
2281                    if (!mDisplayNameCandidate.writableAccount && writableAccount) {
2282                        replace = true;
2283                    } else if (mDisplayNameCandidate.writableAccount == writableAccount) {
2284                        if (NameNormalizer.compareComplexity(displayName,
2285                                mDisplayNameCandidate.displayName) > 0) {
2286                            // New name is more complex than the previously found one
2287                            replace = true;
2288                        }
2289                    }
2290                }
2291            }
2292        }
2293
2294        if (replace) {
2295            mDisplayNameCandidate.rawContactId = rawContactId;
2296            mDisplayNameCandidate.displayName = displayName;
2297            mDisplayNameCandidate.displayNameSource = displayNameSource;
2298            mDisplayNameCandidate.verified = verified;
2299            mDisplayNameCandidate.writableAccount = writableAccount;
2300        }
2301    }
2302
2303    private interface PhotoIdQuery {
2304        final String[] COLUMNS = new String[] {
2305            AccountsColumns.CONCRETE_ACCOUNT_TYPE,
2306            DataColumns.CONCRETE_ID,
2307            Data.IS_SUPER_PRIMARY,
2308            Photo.PHOTO_FILE_ID,
2309        };
2310
2311        int ACCOUNT_TYPE = 0;
2312        int DATA_ID = 1;
2313        int IS_SUPER_PRIMARY = 2;
2314        int PHOTO_FILE_ID = 3;
2315    }
2316
2317    public void updatePhotoId(SQLiteDatabase db, long rawContactId) {
2318
2319        long contactId = mDbHelper.getContactId(rawContactId);
2320        if (contactId == 0) {
2321            return;
2322        }
2323
2324        long bestPhotoId = -1;
2325        long bestPhotoFileId = 0;
2326        int photoPriority = -1;
2327
2328        long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
2329
2330        String tables = Tables.RAW_CONTACTS
2331                + " JOIN " + Tables.ACCOUNTS + " ON ("
2332                    + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
2333                    + ")"
2334                + " JOIN " + Tables.DATA + " ON("
2335                + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
2336                + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
2337                        + Photo.PHOTO + " NOT NULL))";
2338
2339        mSelectionArgs1[0] = String.valueOf(contactId);
2340        final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS,
2341                RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
2342        try {
2343            PhotoEntry bestPhotoEntry = null;
2344            while (c.moveToNext()) {
2345                long dataId = c.getLong(PhotoIdQuery.DATA_ID);
2346                long photoFileId = c.getLong(PhotoIdQuery.PHOTO_FILE_ID);
2347                boolean superPrimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0;
2348                PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
2349
2350                // Note that data set does not come into play here, since accounts are looked up in
2351                // the account manager in the priority resolver.
2352                String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE);
2353                int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
2354                if (superPrimary || hasHigherPhotoPriority(
2355                        photoEntry, priority, bestPhotoEntry, photoPriority)) {
2356                    bestPhotoEntry = photoEntry;
2357                    photoPriority = priority;
2358                    bestPhotoId = dataId;
2359                    bestPhotoFileId = photoFileId;
2360                    if (superPrimary) {
2361                        break;
2362                    }
2363                }
2364            }
2365        } finally {
2366            c.close();
2367        }
2368
2369        if (bestPhotoId == -1) {
2370            mPhotoIdUpdate.bindNull(1);
2371        } else {
2372            mPhotoIdUpdate.bindLong(1, bestPhotoId);
2373        }
2374
2375        if (bestPhotoFileId == 0) {
2376            mPhotoIdUpdate.bindNull(2);
2377        } else {
2378            mPhotoIdUpdate.bindLong(2, bestPhotoFileId);
2379        }
2380
2381        mPhotoIdUpdate.bindLong(3, contactId);
2382        mPhotoIdUpdate.execute();
2383    }
2384
2385    private interface PhotoFileQuery {
2386        final String[] COLUMNS = new String[] {
2387                PhotoFiles.HEIGHT,
2388                PhotoFiles.WIDTH,
2389                PhotoFiles.FILESIZE
2390        };
2391
2392        int HEIGHT = 0;
2393        int WIDTH = 1;
2394        int FILESIZE = 2;
2395    }
2396
2397    private class PhotoEntry implements Comparable<PhotoEntry> {
2398        // Pixel count (width * height) for the image.
2399        final int pixelCount;
2400
2401        // File size (in bytes) of the image.  Not populated if the image is a thumbnail.
2402        final int fileSize;
2403
2404        private PhotoEntry(int pixelCount, int fileSize) {
2405            this.pixelCount = pixelCount;
2406            this.fileSize = fileSize;
2407        }
2408
2409        @Override
2410        public int compareTo(PhotoEntry pe) {
2411            if (pe == null) {
2412                return -1;
2413            }
2414            if (pixelCount == pe.pixelCount) {
2415                return pe.fileSize - fileSize;
2416            } else {
2417                return pe.pixelCount - pixelCount;
2418            }
2419        }
2420    }
2421
2422    private PhotoEntry getPhotoMetadata(SQLiteDatabase db, long photoFileId) {
2423        if (photoFileId == 0) {
2424            // Assume standard thumbnail size.  Don't bother getting a file size for priority;
2425            // we should fall back to photo priority resolver if all we have are thumbnails.
2426            int thumbDim = mContactsProvider.getMaxThumbnailDim();
2427            return new PhotoEntry(thumbDim * thumbDim, 0);
2428        } else {
2429            Cursor c = db.query(Tables.PHOTO_FILES, PhotoFileQuery.COLUMNS, PhotoFiles._ID + "=?",
2430                    new String[]{String.valueOf(photoFileId)}, null, null, null);
2431            try {
2432                if (c.getCount() == 1) {
2433                    c.moveToFirst();
2434                    int pixelCount =
2435                            c.getInt(PhotoFileQuery.HEIGHT) * c.getInt(PhotoFileQuery.WIDTH);
2436                    return new PhotoEntry(pixelCount, c.getInt(PhotoFileQuery.FILESIZE));
2437                }
2438            } finally {
2439                c.close();
2440            }
2441        }
2442        return new PhotoEntry(0, 0);
2443    }
2444
2445    private interface DisplayNameQuery {
2446        String[] COLUMNS = new String[] {
2447            RawContacts._ID,
2448            RawContactsColumns.DISPLAY_NAME,
2449            RawContactsColumns.DISPLAY_NAME_SOURCE,
2450            RawContacts.NAME_VERIFIED,
2451            RawContacts.SOURCE_ID,
2452            RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
2453        };
2454
2455        int _ID = 0;
2456        int DISPLAY_NAME = 1;
2457        int DISPLAY_NAME_SOURCE = 2;
2458        int NAME_VERIFIED = 3;
2459        int SOURCE_ID = 4;
2460        int ACCOUNT_TYPE_AND_DATA_SET = 5;
2461    }
2462
2463    public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) {
2464        long contactId = mDbHelper.getContactId(rawContactId);
2465        if (contactId == 0) {
2466            return;
2467        }
2468
2469        updateDisplayNameForContact(db, contactId);
2470    }
2471
2472    public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) {
2473        boolean lookupKeyUpdateNeeded = false;
2474
2475        mDisplayNameCandidate.clear();
2476
2477        mSelectionArgs1[0] = String.valueOf(contactId);
2478        final Cursor c = db.query(Views.RAW_CONTACTS, DisplayNameQuery.COLUMNS,
2479                RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
2480        try {
2481            while (c.moveToNext()) {
2482                long rawContactId = c.getLong(DisplayNameQuery._ID);
2483                String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME);
2484                int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE);
2485                int nameVerified = c.getInt(DisplayNameQuery.NAME_VERIFIED);
2486                String accountTypeAndDataSet = c.getString(
2487                        DisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET);
2488                processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
2489                        mContactsProvider.isWritableAccountWithDataSet(accountTypeAndDataSet),
2490                        nameVerified != 0);
2491
2492                // If the raw contact has no source id, the lookup key is based on the display
2493                // name, so the lookup key needs to be updated.
2494                lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID);
2495            }
2496        } finally {
2497            c.close();
2498        }
2499
2500        if (mDisplayNameCandidate.rawContactId != -1) {
2501            mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId);
2502            mDisplayNameUpdate.bindLong(2, contactId);
2503            mDisplayNameUpdate.execute();
2504        }
2505
2506        if (lookupKeyUpdateNeeded) {
2507            updateLookupKeyForContact(db, contactId);
2508        }
2509    }
2510
2511
2512    /**
2513     * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the
2514     * specified raw contact.
2515     */
2516    public void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) {
2517
2518        long contactId = mDbHelper.getContactId(rawContactId);
2519        if (contactId == 0) {
2520            return;
2521        }
2522
2523        final SQLiteStatement hasPhoneNumberUpdate = db.compileStatement(
2524                "UPDATE " + Tables.CONTACTS +
2525                " SET " + Contacts.HAS_PHONE_NUMBER + "="
2526                        + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)"
2527                        + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS
2528                        + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
2529                                + " AND " + Phone.NUMBER + " NOT NULL"
2530                                + " AND " + RawContacts.CONTACT_ID + "=?)" +
2531                " WHERE " + Contacts._ID + "=?");
2532        try {
2533            hasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE));
2534            hasPhoneNumberUpdate.bindLong(2, contactId);
2535            hasPhoneNumberUpdate.bindLong(3, contactId);
2536            hasPhoneNumberUpdate.execute();
2537        } finally {
2538            hasPhoneNumberUpdate.close();
2539        }
2540    }
2541
2542    private interface LookupKeyQuery {
2543        String TABLE = Views.RAW_CONTACTS;
2544        String[] COLUMNS = new String[] {
2545            RawContacts._ID,
2546            RawContactsColumns.DISPLAY_NAME,
2547            RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
2548            RawContacts.ACCOUNT_NAME,
2549            RawContacts.SOURCE_ID,
2550        };
2551
2552        int ID = 0;
2553        int DISPLAY_NAME = 1;
2554        int ACCOUNT_TYPE_AND_DATA_SET = 2;
2555        int ACCOUNT_NAME = 3;
2556        int SOURCE_ID = 4;
2557    }
2558
2559    public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
2560        long contactId = mDbHelper.getContactId(rawContactId);
2561        if (contactId == 0) {
2562            return;
2563        }
2564
2565        updateLookupKeyForContact(db, contactId);
2566    }
2567
2568    private void updateLookupKeyForContact(SQLiteDatabase db, long contactId) {
2569        String lookupKey = computeLookupKeyForContact(db, contactId);
2570
2571        if (lookupKey == null) {
2572            mLookupKeyUpdate.bindNull(1);
2573        } else {
2574            mLookupKeyUpdate.bindString(1, Uri.encode(lookupKey));
2575        }
2576        mLookupKeyUpdate.bindLong(2, contactId);
2577
2578        mLookupKeyUpdate.execute();
2579    }
2580
2581    protected String computeLookupKeyForContact(SQLiteDatabase db, long contactId) {
2582        StringBuilder sb = new StringBuilder();
2583        mSelectionArgs1[0] = String.valueOf(contactId);
2584        final Cursor c = db.query(LookupKeyQuery.TABLE, LookupKeyQuery.COLUMNS,
2585                RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID);
2586        try {
2587            while (c.moveToNext()) {
2588                ContactLookupKey.appendToLookupKey(sb,
2589                        c.getString(LookupKeyQuery.ACCOUNT_TYPE_AND_DATA_SET),
2590                        c.getString(LookupKeyQuery.ACCOUNT_NAME),
2591                        c.getLong(LookupKeyQuery.ID),
2592                        c.getString(LookupKeyQuery.SOURCE_ID),
2593                        c.getString(LookupKeyQuery.DISPLAY_NAME));
2594            }
2595        } finally {
2596            c.close();
2597        }
2598        return sb.length() == 0 ? null : sb.toString();
2599    }
2600
2601    /**
2602     * Execute {@link SQLiteStatement} that will update the
2603     * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}.
2604     */
2605    public void updateStarred(long rawContactId) {
2606        long contactId = mDbHelper.getContactId(rawContactId);
2607        if (contactId == 0) {
2608            return;
2609        }
2610
2611        mStarredUpdate.bindLong(1, contactId);
2612        mStarredUpdate.execute();
2613    }
2614
2615    /**
2616     * Execute {@link SQLiteStatement} that will update the
2617     * {@link Contacts#PINNED} flag for the given {@link RawContacts#_ID}.
2618     */
2619    public void updatePinned(long rawContactId) {
2620        long contactId = mDbHelper.getContactId(rawContactId);
2621        if (contactId == 0) {
2622            return;
2623        }
2624        mPinnedUpdate.bindLong(1, contactId);
2625        mPinnedUpdate.execute();
2626    }
2627
2628    /**
2629     * Finds matching contacts and returns a cursor on those.
2630     */
2631    public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb,
2632            String[] projection, long contactId, int maxSuggestions, String filter,
2633            ArrayList<AggregationSuggestionParameter> parameters) {
2634        final SQLiteDatabase db = mDbHelper.getReadableDatabase();
2635        db.beginTransaction();
2636        try {
2637            List<MatchScore> bestMatches = findMatchingContacts(db, contactId, parameters);
2638            return queryMatchingContacts(qb, db, projection, bestMatches, maxSuggestions, filter);
2639        } finally {
2640            db.endTransaction();
2641        }
2642    }
2643
2644    private interface ContactIdQuery {
2645        String[] COLUMNS = new String[] {
2646            Contacts._ID
2647        };
2648
2649        int _ID = 0;
2650    }
2651
2652    /**
2653     * Loads contacts with specified IDs and returns them in the order of IDs in the
2654     * supplied list.
2655     */
2656    private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db,
2657            String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) {
2658        StringBuilder sb = new StringBuilder();
2659        sb.append(Contacts._ID);
2660        sb.append(" IN (");
2661        for (int i = 0; i < bestMatches.size(); i++) {
2662            MatchScore matchScore = bestMatches.get(i);
2663            if (i != 0) {
2664                sb.append(",");
2665            }
2666            sb.append(matchScore.getContactId());
2667        }
2668        sb.append(")");
2669
2670        if (!TextUtils.isEmpty(filter)) {
2671            sb.append(" AND " + Contacts._ID + " IN ");
2672            mContactsProvider.appendContactFilterAsNestedQuery(sb, filter);
2673        }
2674
2675        // Run a query and find ids of best matching contacts satisfying the filter (if any)
2676        HashSet<Long> foundIds = new HashSet<Long>();
2677        Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(),
2678                null, null, null, null);
2679        try {
2680            while(cursor.moveToNext()) {
2681                foundIds.add(cursor.getLong(ContactIdQuery._ID));
2682            }
2683        } finally {
2684            cursor.close();
2685        }
2686
2687        // Exclude all contacts that did not match the filter
2688        Iterator<MatchScore> iter = bestMatches.iterator();
2689        while (iter.hasNext()) {
2690            long id = iter.next().getContactId();
2691            if (!foundIds.contains(id)) {
2692                iter.remove();
2693            }
2694        }
2695
2696        // Limit the number of returned suggestions
2697        final List<MatchScore> limitedMatches;
2698        if (bestMatches.size() > maxSuggestions) {
2699            limitedMatches = bestMatches.subList(0, maxSuggestions);
2700        } else {
2701            limitedMatches = bestMatches;
2702        }
2703
2704        // Build an in-clause with the remaining contact IDs
2705        sb.setLength(0);
2706        sb.append(Contacts._ID);
2707        sb.append(" IN (");
2708        for (int i = 0; i < limitedMatches.size(); i++) {
2709            MatchScore matchScore = limitedMatches.get(i);
2710            if (i != 0) {
2711                sb.append(",");
2712            }
2713            sb.append(matchScore.getContactId());
2714        }
2715        sb.append(")");
2716
2717        // Run the final query with the required projection and contact IDs found by the first query
2718        cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID);
2719
2720        // Build a sorted list of discovered IDs
2721        ArrayList<Long> sortedContactIds = new ArrayList<Long>(limitedMatches.size());
2722        for (MatchScore matchScore : limitedMatches) {
2723            sortedContactIds.add(matchScore.getContactId());
2724        }
2725
2726        Collections.sort(sortedContactIds);
2727
2728        // Map cursor indexes according to the descending order of match scores
2729        int[] positionMap = new int[limitedMatches.size()];
2730        for (int i = 0; i < positionMap.length; i++) {
2731            long id = limitedMatches.get(i).getContactId();
2732            positionMap[i] = sortedContactIds.indexOf(id);
2733        }
2734
2735        return new ReorderingCursorWrapper(cursor, positionMap);
2736    }
2737
2738    /**
2739     * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
2740     * descending order of match score.
2741     * @param parameters
2742     */
2743    private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId,
2744            ArrayList<AggregationSuggestionParameter> parameters) {
2745
2746        MatchCandidateList candidates = new MatchCandidateList();
2747        ContactMatcher matcher = new ContactMatcher();
2748
2749        // Don't aggregate a contact with itself
2750        matcher.keepOut(contactId);
2751
2752        if (parameters == null || parameters.size() == 0) {
2753            final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
2754                    RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
2755            try {
2756                while (c.moveToNext()) {
2757                    long rawContactId = c.getLong(RawContactIdQuery.RAW_CONTACT_ID);
2758                    updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
2759                            matcher);
2760                }
2761            } finally {
2762                c.close();
2763            }
2764        } else {
2765            updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates,
2766                    matcher, parameters);
2767        }
2768
2769        return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST);
2770    }
2771
2772    /**
2773     * Computes scores for contacts that have matching data rows.
2774     */
2775    private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
2776            long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
2777
2778        updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
2779        updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
2780        updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
2781        updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
2782        loadNameMatchCandidates(db, rawContactId, candidates, false);
2783        lookupApproximateNameMatches(db, candidates, matcher);
2784    }
2785
2786    private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
2787            MatchCandidateList candidates, ContactMatcher matcher,
2788            ArrayList<AggregationSuggestionParameter> parameters) {
2789        for (AggregationSuggestionParameter parameter : parameters) {
2790            if (AggregationSuggestions.PARAMETER_MATCH_NAME.equals(parameter.kind)) {
2791                updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher);
2792            }
2793
2794            // TODO: add support for other parameter kinds
2795        }
2796    }
2797}
2798