AbstractContactAggregator.java revision e2e9ac275e487ce558579ee65ff8f122cf498b07
1/*
2 * Copyright (C) 2015 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 com.android.internal.annotations.VisibleForTesting;
20import com.android.providers.contacts.ContactLookupKey;
21import com.android.providers.contacts.ContactsDatabaseHelper;
22import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
23import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
24import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
25import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
26import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
27import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
28import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
29import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
30import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
31import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
32import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
33import com.android.providers.contacts.ContactsDatabaseHelper.Views;
34import com.android.providers.contacts.ContactsProvider2;
35import com.android.providers.contacts.NameLookupBuilder;
36import com.android.providers.contacts.NameNormalizer;
37import com.android.providers.contacts.NameSplitter;
38import com.android.providers.contacts.PhotoPriorityResolver;
39import com.android.providers.contacts.ReorderingCursorWrapper;
40import com.android.providers.contacts.TransactionContext;
41import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
42import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper;
43import com.android.providers.contacts.aggregation.util.ContactMatcher;
44import com.android.providers.contacts.aggregation.util.MatchScore;
45import com.android.providers.contacts.util.Clock;
46import com.google.android.collect.Maps;
47import com.google.common.collect.HashMultimap;
48import com.google.common.collect.Multimap;
49
50import android.database.Cursor;
51import android.database.DatabaseUtils;
52import android.database.sqlite.SQLiteDatabase;
53import android.database.sqlite.SQLiteQueryBuilder;
54import android.database.sqlite.SQLiteStatement;
55import android.net.Uri;
56import android.provider.ContactsContract.AggregationExceptions;
57import android.provider.ContactsContract.CommonDataKinds.Email;
58import android.provider.ContactsContract.CommonDataKinds.Identity;
59import android.provider.ContactsContract.CommonDataKinds.Phone;
60import android.provider.ContactsContract.CommonDataKinds.Photo;
61import android.provider.ContactsContract.Contacts;
62import android.provider.ContactsContract.Data;
63import android.provider.ContactsContract.DisplayNameSources;
64import android.provider.ContactsContract.FullNameStyle;
65import android.provider.ContactsContract.PhotoFiles;
66import android.provider.ContactsContract.PinnedPositions;
67import android.provider.ContactsContract.RawContacts;
68import android.provider.ContactsContract.StatusUpdates;
69import android.text.TextUtils;
70import android.util.EventLog;
71import android.util.Log;
72
73import java.util.ArrayList;
74import java.util.Collections;
75import java.util.HashMap;
76import java.util.HashSet;
77import java.util.Iterator;
78import java.util.List;
79import java.util.Locale;
80import java.util.Set;
81
82/**
83 * Base class of contact aggregator and profile aggregator
84 */
85public abstract class AbstractContactAggregator {
86
87    protected static final String TAG = "ContactAggregator";
88
89    protected static final boolean DEBUG_LOGGING = Log.isLoggable(TAG, Log.DEBUG);
90    protected static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
91
92    protected static final String STRUCTURED_NAME_BASED_LOOKUP_SQL =
93            NameLookupColumns.NAME_TYPE + " IN ("
94                    + NameLookupType.NAME_EXACT + ","
95                    + NameLookupType.NAME_VARIANT + ","
96                    + NameLookupType.NAME_COLLATION_KEY + ")";
97
98
99    /**
100     * SQL statement that sets the {@link ContactsColumns#LAST_STATUS_UPDATE_ID} column
101     * on the contact to point to the latest social status update.
102     */
103    protected static final String UPDATE_LAST_STATUS_UPDATE_ID_SQL =
104            "UPDATE " + Tables.CONTACTS +
105            " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
106                    "(SELECT " + DataColumns.CONCRETE_ID +
107                    " FROM " + Tables.STATUS_UPDATES +
108                    " JOIN " + Tables.DATA +
109                    "   ON (" + StatusUpdatesColumns.DATA_ID + "="
110                            + DataColumns.CONCRETE_ID + ")" +
111                    " JOIN " + Tables.RAW_CONTACTS +
112                    "   ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "="
113                            + RawContactsColumns.CONCRETE_ID + ")" +
114                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
115                    " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
116                            + StatusUpdates.STATUS +
117                    " LIMIT 1)" +
118            " WHERE " + ContactsColumns.CONCRETE_ID + "=?";
119
120    // From system/core/logcat/event-log-tags
121    // aggregator [time, count] will be logged for each aggregator cycle.
122    // For the query (as opposed to the merge), count will be negative
123    static final int LOG_SYNC_CONTACTS_AGGREGATION = 2747;
124
125    // If we encounter more than this many contacts with matching names, aggregate only this many
126    protected static final int PRIMARY_HIT_LIMIT = 15;
127    protected static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT);
128
129    // If we encounter more than this many contacts with matching phone number or email,
130    // don't attempt to aggregate - this is likely an error or a shared corporate data element.
131    protected static final int SECONDARY_HIT_LIMIT = 20;
132    protected static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT);
133
134    // If we encounter no less than this many raw contacts in the best matching contact during
135    // aggregation, don't attempt to aggregate - this is likely an error or a shared corporate
136    // data element.
137    @VisibleForTesting
138    static final int AGGREGATION_CONTACT_SIZE_LIMIT = 50;
139
140    // If we encounter more than this many contacts with matching name during aggregation
141    // suggestion lookup, ignore the remaining results.
142    protected static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100;
143
144    protected final ContactsProvider2 mContactsProvider;
145    protected final ContactsDatabaseHelper mDbHelper;
146    protected PhotoPriorityResolver mPhotoPriorityResolver;
147    protected final NameSplitter mNameSplitter;
148    protected final CommonNicknameCache mCommonNicknameCache;
149
150    protected boolean mEnabled = true;
151
152    /**
153     * Precompiled sql statement for setting an aggregated presence
154     */
155    protected SQLiteStatement mRawContactCountQuery;
156    protected SQLiteStatement mAggregatedPresenceDelete;
157    protected SQLiteStatement mAggregatedPresenceReplace;
158    protected SQLiteStatement mPresenceContactIdUpdate;
159    protected SQLiteStatement mMarkForAggregation;
160    protected SQLiteStatement mPhotoIdUpdate;
161    protected SQLiteStatement mDisplayNameUpdate;
162    protected SQLiteStatement mLookupKeyUpdate;
163    protected SQLiteStatement mStarredUpdate;
164    protected SQLiteStatement mSendToVoicemailUpdate;
165    protected SQLiteStatement mPinnedUpdate;
166    protected SQLiteStatement mContactIdAndMarkAggregatedUpdate;
167    protected SQLiteStatement mContactIdUpdate;
168    protected SQLiteStatement mContactUpdate;
169    protected SQLiteStatement mContactInsert;
170    protected SQLiteStatement mResetPinnedForRawContact;
171
172    protected HashMap<Long, Integer> mRawContactsMarkedForAggregation = Maps.newHashMap();
173
174    protected String[] mSelectionArgs1 = new String[1];
175    protected String[] mSelectionArgs2 = new String[2];
176
177    protected long mMimeTypeIdIdentity;
178    protected long mMimeTypeIdEmail;
179    protected long mMimeTypeIdPhoto;
180    protected long mMimeTypeIdPhone;
181    protected String mRawContactsQueryByRawContactId;
182    protected String mRawContactsQueryByContactId;
183    protected StringBuilder mSb = new StringBuilder();
184    protected MatchCandidateList mCandidates = new MatchCandidateList();
185    protected DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate();
186
187    /**
188     * Parameter for the suggestion lookup query.
189     */
190    public static final class AggregationSuggestionParameter {
191        public final String kind;
192        public final String value;
193
194        public AggregationSuggestionParameter(String kind, String value) {
195            this.kind = kind;
196            this.value = value;
197        }
198    }
199
200    /**
201     * Captures a potential match for a given name. The matching algorithm
202     * constructs a bunch of NameMatchCandidate objects for various potential matches
203     * and then executes the search in bulk.
204     */
205    protected static class NameMatchCandidate {
206        String mName;
207        int mLookupType;
208
209        public NameMatchCandidate(String name, int nameLookupType) {
210            mName = name;
211            mLookupType = nameLookupType;
212        }
213    }
214
215    /**
216     * A list of {@link NameMatchCandidate} that keeps its elements even when the list is
217     * truncated. This is done for optimization purposes to avoid excessive object allocation.
218     */
219    protected static class MatchCandidateList {
220        protected final ArrayList<NameMatchCandidate> mList = new ArrayList<NameMatchCandidate>();
221        protected int mCount;
222
223        /**
224         * Adds a {@link NameMatchCandidate} element or updates the next one if it already exists.
225         */
226        public void add(String name, int nameLookupType) {
227            if (mCount >= mList.size()) {
228                mList.add(new NameMatchCandidate(name, nameLookupType));
229            } else {
230                NameMatchCandidate candidate = mList.get(mCount);
231                candidate.mName = name;
232                candidate.mLookupType = nameLookupType;
233            }
234            mCount++;
235        }
236
237        public void clear() {
238            mCount = 0;
239        }
240
241        public boolean isEmpty() {
242            return mCount == 0;
243        }
244    }
245
246    /**
247     * A convenience class used in the algorithm that figures out which of available
248     * display names to use for an aggregate contact.
249     */
250    private static class DisplayNameCandidate {
251        long rawContactId;
252        String displayName;
253        int displayNameSource;
254        boolean isNameSuperPrimary;
255        boolean writableAccount;
256
257        public DisplayNameCandidate() {
258            clear();
259        }
260
261        public void clear() {
262            rawContactId = -1;
263            displayName = null;
264            displayNameSource = DisplayNameSources.UNDEFINED;
265            isNameSuperPrimary = false;
266            writableAccount = false;
267        }
268    }
269
270    /**
271     * Constructor.
272     */
273    public AbstractContactAggregator(ContactsProvider2 contactsProvider,
274            ContactsDatabaseHelper contactsDatabaseHelper,
275            PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter,
276            CommonNicknameCache commonNicknameCache) {
277        mContactsProvider = contactsProvider;
278        mDbHelper = contactsDatabaseHelper;
279        mPhotoPriorityResolver = photoPriorityResolver;
280        mNameSplitter = nameSplitter;
281        mCommonNicknameCache = commonNicknameCache;
282
283        SQLiteDatabase db = mDbHelper.getReadableDatabase();
284
285        // Since we have no way of determining which custom status was set last,
286        // we'll just pick one randomly.  We are using MAX as an approximation of randomness
287        final String replaceAggregatePresenceSql =
288                "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
289                + AggregatedPresenceColumns.CONTACT_ID + ", "
290                + StatusUpdates.PRESENCE + ", "
291                + StatusUpdates.CHAT_CAPABILITY + ")"
292                + " SELECT " + PresenceColumns.CONTACT_ID + ","
293                + StatusUpdates.PRESENCE + ","
294                + StatusUpdates.CHAT_CAPABILITY
295                + " FROM " + Tables.PRESENCE
296                + " WHERE "
297                + " (" + StatusUpdates.PRESENCE
298                +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
299                + " = (SELECT "
300                + "MAX (" + StatusUpdates.PRESENCE
301                +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
302                + " FROM " + Tables.PRESENCE
303                + " WHERE " + PresenceColumns.CONTACT_ID
304                + "=?)"
305                + " AND " + PresenceColumns.CONTACT_ID
306                + "=?;";
307        mAggregatedPresenceReplace = db.compileStatement(replaceAggregatePresenceSql);
308
309        mRawContactCountQuery = db.compileStatement(
310                "SELECT COUNT(" + RawContacts._ID + ")" +
311                " FROM " + Tables.RAW_CONTACTS +
312                " WHERE " + RawContacts.CONTACT_ID + "=?"
313                        + " AND " + RawContacts._ID + "<>?");
314
315        mAggregatedPresenceDelete = db.compileStatement(
316                "DELETE FROM " + Tables.AGGREGATED_PRESENCE +
317                " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?");
318
319        mMarkForAggregation = db.compileStatement(
320                "UPDATE " + Tables.RAW_CONTACTS +
321                " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=1" +
322                " WHERE " + RawContacts._ID + "=?"
323                        + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0");
324
325        mPhotoIdUpdate = db.compileStatement(
326                "UPDATE " + Tables.CONTACTS +
327                " SET " + Contacts.PHOTO_ID + "=?," + Contacts.PHOTO_FILE_ID + "=? " +
328                " WHERE " + Contacts._ID + "=?");
329
330        mDisplayNameUpdate = db.compileStatement(
331                "UPDATE " + Tables.CONTACTS +
332                " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " +
333                " WHERE " + Contacts._ID + "=?");
334
335        mLookupKeyUpdate = db.compileStatement(
336                "UPDATE " + Tables.CONTACTS +
337                " SET " + Contacts.LOOKUP_KEY + "=? " +
338                " WHERE " + Contacts._ID + "=?");
339
340        mStarredUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
341                + Contacts.STARRED + "=(SELECT (CASE WHEN COUNT(" + RawContacts.STARRED
342                + ")=0 THEN 0 ELSE 1 END) FROM " + Tables.RAW_CONTACTS + " WHERE "
343                + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND "
344                + RawContacts.STARRED + "=1)" + " WHERE " + Contacts._ID + "=?");
345
346        mSendToVoicemailUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
347                + Contacts.SEND_TO_VOICEMAIL + "=(CASE WHEN (SELECT COUNT( "
348                + RawContacts.SEND_TO_VOICEMAIL + ") FROM " + Tables.RAW_CONTACTS
349                + " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + " AND "
350                + RawContacts.SEND_TO_VOICEMAIL + "=1) = (SELECT COUNT("
351                + RawContacts.SEND_TO_VOICEMAIL + ") FROM " + Tables.RAW_CONTACTS + " WHERE "
352                + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID
353                + ") THEN 1 ELSE 0 END)" + " WHERE " + Contacts._ID + "=?");
354
355        mPinnedUpdate = db.compileStatement("UPDATE " + Tables.CONTACTS + " SET "
356                + Contacts.PINNED + " = IFNULL((SELECT MIN(" + RawContacts.PINNED + ") FROM "
357                + Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "="
358                + ContactsColumns.CONCRETE_ID + " AND " + RawContacts.PINNED + ">"
359                + PinnedPositions.UNPINNED + ")," + PinnedPositions.UNPINNED + ") "
360                + "WHERE " + Contacts._ID + "=?");
361
362        mContactIdAndMarkAggregatedUpdate = db.compileStatement(
363                "UPDATE " + Tables.RAW_CONTACTS +
364                " SET " + RawContacts.CONTACT_ID + "=?, "
365                        + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
366                " WHERE " + RawContacts._ID + "=?");
367
368        mContactIdUpdate = db.compileStatement(
369                "UPDATE " + Tables.RAW_CONTACTS +
370                " SET " + RawContacts.CONTACT_ID + "=?" +
371                " WHERE " + RawContacts._ID + "=?");
372
373        mPresenceContactIdUpdate = db.compileStatement(
374                "UPDATE " + Tables.PRESENCE +
375                " SET " + PresenceColumns.CONTACT_ID + "=?" +
376                " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?");
377
378        mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL);
379        mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL);
380
381        mResetPinnedForRawContact = db.compileStatement(
382                "UPDATE " + Tables.RAW_CONTACTS +
383                " SET " + RawContacts.PINNED + "=" + PinnedPositions.UNPINNED +
384                " WHERE " + RawContacts._ID + "=?");
385
386        mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
387        mMimeTypeIdIdentity = mDbHelper.getMimeTypeId(Identity.CONTENT_ITEM_TYPE);
388        mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
389        mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
390
391        // Query used to retrieve data from raw contacts to populate the corresponding aggregate
392        mRawContactsQueryByRawContactId = String.format(Locale.US,
393                RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID,
394                mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone);
395
396        mRawContactsQueryByContactId = String.format(Locale.US,
397                RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
398                mDbHelper.getMimeTypeIdForStructuredName(), mMimeTypeIdPhoto, mMimeTypeIdPhone);
399    }
400
401    public final void setEnabled(boolean enabled) {
402        mEnabled = enabled;
403    }
404
405    public final boolean isEnabled() {
406        return mEnabled;
407    }
408
409    protected interface AggregationQuery {
410        String SQL =
411                "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
412                        ", " + RawContactsColumns.ACCOUNT_ID +
413                " FROM " + Tables.RAW_CONTACTS +
414                " WHERE " + RawContacts._ID + " IN(";
415
416        int _ID = 0;
417        int CONTACT_ID = 1;
418        int ACCOUNT_ID = 2;
419    }
420
421    /**
422     * Aggregate all raw contacts that were marked for aggregation in the current transaction.
423     * Call just before committing the transaction.
424     */
425    // Overridden by ProfileAggregator.
426    public void aggregateInTransaction(TransactionContext txContext, SQLiteDatabase db) {
427        final int markedCount = mRawContactsMarkedForAggregation.size();
428        if (markedCount == 0) {
429            return;
430        }
431
432        final long start = System.currentTimeMillis();
433        if (DEBUG_LOGGING) {
434            Log.d(TAG, "aggregateInTransaction for " + markedCount + " contacts");
435        }
436
437        EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -markedCount);
438
439        int index = 0;
440
441        // We don't use the cached string builder (namely mSb)  here, as this string can be very
442        // long when upgrading (where we re-aggregate all visible contacts) and StringBuilder won't
443        // shrink the internal storage.
444        // Note: don't use selection args here.  We just include all IDs directly in the selection,
445        // because there's a limit for the number of parameters in a query.
446        final StringBuilder sbQuery = new StringBuilder();
447        sbQuery.append(AggregationQuery.SQL);
448        for (long rawContactId : mRawContactsMarkedForAggregation.keySet()) {
449            if (index > 0) {
450                sbQuery.append(',');
451            }
452            sbQuery.append(rawContactId);
453            index++;
454        }
455
456        sbQuery.append(')');
457
458        final long[] rawContactIds;
459        final long[] contactIds;
460        final long[] accountIds;
461        final int actualCount;
462        final Cursor c = db.rawQuery(sbQuery.toString(), null);
463        try {
464            actualCount = c.getCount();
465            rawContactIds = new long[actualCount];
466            contactIds = new long[actualCount];
467            accountIds = new long[actualCount];
468
469            index = 0;
470            while (c.moveToNext()) {
471                rawContactIds[index] = c.getLong(AggregationQuery._ID);
472                contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
473                accountIds[index] = c.getLong(AggregationQuery.ACCOUNT_ID);
474                index++;
475            }
476        } finally {
477            c.close();
478        }
479
480        if (DEBUG_LOGGING) {
481            Log.d(TAG, "aggregateInTransaction: initial query done.");
482        }
483
484        for (int i = 0; i < actualCount; i++) {
485            aggregateContact(txContext, db, rawContactIds[i], accountIds[i], contactIds[i],
486                    mCandidates);
487        }
488
489        long elapsedTime = System.currentTimeMillis() - start;
490        EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, actualCount);
491
492        if (DEBUG_LOGGING) {
493            Log.d(TAG, "Contact aggregation complete: " + actualCount +
494                    (actualCount == 0 ? "" : ", " + (elapsedTime / actualCount)
495                            + " ms per raw contact"));
496        }
497    }
498
499    @SuppressWarnings("deprecation")
500    public final void triggerAggregation(TransactionContext txContext, long rawContactId) {
501        if (!mEnabled) {
502            return;
503        }
504
505        int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
506        switch (aggregationMode) {
507            case RawContacts.AGGREGATION_MODE_DISABLED:
508                break;
509
510            case RawContacts.AGGREGATION_MODE_DEFAULT: {
511                markForAggregation(rawContactId, aggregationMode, false);
512                break;
513            }
514
515            case RawContacts.AGGREGATION_MODE_SUSPENDED: {
516                long contactId = mDbHelper.getContactId(rawContactId);
517
518                if (contactId != 0) {
519                    updateAggregateData(txContext, contactId);
520                }
521                break;
522            }
523
524            case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
525                aggregateContact(txContext, mDbHelper.getWritableDatabase(), rawContactId);
526                break;
527            }
528        }
529    }
530
531    public final void clearPendingAggregations() {
532        // HashMap woulnd't shrink the internal table once expands it, so let's just re-create
533        // a new one instead of clear()ing it.
534        mRawContactsMarkedForAggregation = Maps.newHashMap();
535    }
536
537    public final void markNewForAggregation(long rawContactId, int aggregationMode) {
538        mRawContactsMarkedForAggregation.put(rawContactId, aggregationMode);
539    }
540
541    public final void markForAggregation(long rawContactId, int aggregationMode, boolean force) {
542        final int effectiveAggregationMode;
543        if (!force && mRawContactsMarkedForAggregation.containsKey(rawContactId)) {
544            // As per ContactsContract documentation, default aggregation mode
545            // does not override a previously set mode
546            if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
547                effectiveAggregationMode = mRawContactsMarkedForAggregation.get(rawContactId);
548            } else {
549                effectiveAggregationMode = aggregationMode;
550            }
551        } else {
552            mMarkForAggregation.bindLong(1, rawContactId);
553            mMarkForAggregation.execute();
554            effectiveAggregationMode = aggregationMode;
555        }
556
557        mRawContactsMarkedForAggregation.put(rawContactId, effectiveAggregationMode);
558    }
559
560    private static class RawContactIdAndAggregationModeQuery {
561        public static final String TABLE = Tables.RAW_CONTACTS;
562
563        public static final String[] COLUMNS = {RawContacts._ID, RawContacts.AGGREGATION_MODE};
564
565        public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
566
567        public static final int _ID = 0;
568        public static final int AGGREGATION_MODE = 1;
569    }
570
571    /**
572     * Marks all constituent raw contacts of an aggregated contact for re-aggregation.
573     */
574    protected final void markContactForAggregation(SQLiteDatabase db, long contactId) {
575        mSelectionArgs1[0] = String.valueOf(contactId);
576        Cursor cursor = db.query(RawContactIdAndAggregationModeQuery.TABLE,
577                RawContactIdAndAggregationModeQuery.COLUMNS,
578                RawContactIdAndAggregationModeQuery.SELECTION, mSelectionArgs1, null, null, null);
579        try {
580            if (cursor.moveToFirst()) {
581                long rawContactId = cursor.getLong(RawContactIdAndAggregationModeQuery._ID);
582                int aggregationMode = cursor.getInt(
583                        RawContactIdAndAggregationModeQuery.AGGREGATION_MODE);
584                // Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED.
585                // (Also just ignore deprecated AGGREGATION_MODE_IMMEDIATE)
586                if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
587                    markForAggregation(rawContactId, aggregationMode, true);
588                }
589            }
590        } finally {
591            cursor.close();
592        }
593    }
594
595    /**
596     * Mark all visible contacts for re-aggregation.
597     *
598     * - Set {@link RawContactsColumns#AGGREGATION_NEEDED} For all visible raw_contacts with
599     *   {@link RawContacts#AGGREGATION_MODE_DEFAULT}.
600     * - Also put them into {@link #mRawContactsMarkedForAggregation}.
601     */
602    public final int markAllVisibleForAggregation(SQLiteDatabase db) {
603        final long start = System.currentTimeMillis();
604
605        // Set AGGREGATION_NEEDED for all visible raw_cotnacts with AGGREGATION_MODE_DEFAULT.
606        // (Don't re-aggregate AGGREGATION_MODE_SUSPENDED / AGGREGATION_MODE_DISABLED)
607        db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " +
608                RawContactsColumns.AGGREGATION_NEEDED + "=1" +
609                " WHERE " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY +
610                " AND " + RawContacts.AGGREGATION_MODE + "=" + RawContacts.AGGREGATION_MODE_DEFAULT
611                );
612
613        final int count;
614        final Cursor cursor = db.rawQuery("SELECT " + RawContacts._ID +
615                " FROM " + Tables.RAW_CONTACTS +
616                " WHERE " + RawContactsColumns.AGGREGATION_NEEDED + "=1 AND " +
617                RawContacts.DELETED + "=0", null);
618        try {
619            count = cursor.getCount();
620            cursor.moveToPosition(-1);
621            while (cursor.moveToNext()) {
622                final long rawContactId = cursor.getLong(0);
623                mRawContactsMarkedForAggregation.put(rawContactId,
624                        RawContacts.AGGREGATION_MODE_DEFAULT);
625            }
626        } finally {
627            cursor.close();
628        }
629
630        final long end = System.currentTimeMillis();
631        Log.i(TAG, "Marked all visible contacts for aggregation: " + count + " raw contacts, " +
632                (end - start) + " ms");
633        return count;
634    }
635
636    /**
637     * Creates a new contact based on the given raw contact.  Does not perform aggregation.  Returns
638     * the ID of the contact that was created.
639     */
640    // Overridden by ProfileAggregator.
641    public long onRawContactInsert(
642            TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
643        long contactId = insertContact(db, rawContactId);
644        setContactId(rawContactId, contactId);
645        mDbHelper.updateContactVisible(txContext, contactId);
646        return contactId;
647    }
648
649    protected final long insertContact(SQLiteDatabase db, long rawContactId) {
650        mSelectionArgs1[0] = String.valueOf(rawContactId);
651        computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
652        return mContactInsert.executeInsert();
653    }
654
655    private static final class RawContactIdAndAccountQuery {
656        public static final String TABLE = Tables.RAW_CONTACTS;
657
658        public static final String[] COLUMNS = {
659                RawContacts.CONTACT_ID,
660                RawContactsColumns.ACCOUNT_ID
661        };
662
663        public static final String SELECTION = RawContacts._ID + "=?";
664
665        public static final int CONTACT_ID = 0;
666        public static final int ACCOUNT_ID = 1;
667    }
668
669    // Overridden by ProfileAggregator.
670    public void aggregateContact(
671            TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
672        if (!mEnabled) {
673            return;
674        }
675
676        MatchCandidateList candidates = new MatchCandidateList();
677
678        long contactId = 0;
679        long accountId = 0;
680        mSelectionArgs1[0] = String.valueOf(rawContactId);
681        Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE,
682                RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION,
683                mSelectionArgs1, null, null, null);
684        try {
685            if (cursor.moveToFirst()) {
686                contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID);
687                accountId = cursor.getLong(RawContactIdAndAccountQuery.ACCOUNT_ID);
688            }
689        } finally {
690            cursor.close();
691        }
692
693        aggregateContact(txContext, db, rawContactId, accountId, contactId,
694                candidates);
695    }
696
697    public void updateAggregateData(TransactionContext txContext, long contactId) {
698        if (!mEnabled) {
699            return;
700        }
701
702        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
703        computeAggregateData(db, contactId, mContactUpdate);
704        mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
705        mContactUpdate.execute();
706
707        mDbHelper.updateContactVisible(txContext, contactId);
708        updateAggregatedStatusUpdate(contactId);
709    }
710
711    protected final void updateAggregatedStatusUpdate(long contactId) {
712        mAggregatedPresenceReplace.bindLong(1, contactId);
713        mAggregatedPresenceReplace.bindLong(2, contactId);
714        mAggregatedPresenceReplace.execute();
715        updateLastStatusUpdateId(contactId);
716    }
717
718    /**
719     * Adjusts the reference to the latest status update for the specified contact.
720     */
721    public final void updateLastStatusUpdateId(long contactId) {
722        String contactIdString = String.valueOf(contactId);
723        mDbHelper.getWritableDatabase().execSQL(UPDATE_LAST_STATUS_UPDATE_ID_SQL,
724                new String[]{contactIdString, contactIdString});
725    }
726
727    /**
728     * Given a specific raw contact, finds all matching aggregate contacts and chooses the one
729     * with the highest match score.  If no such contact is found, creates a new contact.
730     */
731    abstract void aggregateContact(TransactionContext txContext, SQLiteDatabase db,
732            long rawContactId, long accountId, long currentContactId,
733            MatchCandidateList candidates);
734
735
736    protected interface RawContactMatchingSelectionStatement {
737        String SELECT_COUNT = "SELECT count(*) ";
738        String SELECT_ID = "SELECT d1." + Data.RAW_CONTACT_ID + ",d2." + Data.RAW_CONTACT_ID;
739    }
740
741    /**
742     * Build sql to check if there is any identity match/mis-match between two sets of raw contact
743     * ids on the same namespace.
744     */
745    protected final String buildIdentityMatchingSql(String rawContactIdSet1,
746            String rawContactIdSet2, boolean isIdentityMatching, boolean countOnly) {
747        final String identityType = String.valueOf(mMimeTypeIdIdentity);
748        final String matchingOperator = (isIdentityMatching) ? "=" : "!=";
749        final String sql =
750                " FROM " + Tables.DATA + " AS d1" +
751                " JOIN " + Tables.DATA + " AS d2" +
752                        " ON (d1." + Identity.IDENTITY + matchingOperator +
753                        " d2." + Identity.IDENTITY + " AND" +
754                        " d1." + Identity.NAMESPACE + " = d2." + Identity.NAMESPACE + " )" +
755                " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + identityType +
756                " AND d2." + DataColumns.MIMETYPE_ID + " = " + identityType +
757                " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
758                " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
759        return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
760                RawContactMatchingSelectionStatement.SELECT_ID + sql;
761    }
762
763    protected final String buildEmailMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
764            boolean countOnly) {
765        final String emailType = String.valueOf(mMimeTypeIdEmail);
766        final String sql =
767                " FROM " + Tables.DATA + " AS d1" +
768                " JOIN " + Tables.DATA + " AS d2" +
769                        " ON d1." + Email.ADDRESS + "= d2." + Email.ADDRESS +
770                " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + emailType +
771                " AND d2." + DataColumns.MIMETYPE_ID + " = " + emailType +
772                " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
773                " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
774        return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
775                RawContactMatchingSelectionStatement.SELECT_ID + sql;
776    }
777
778    protected final String buildPhoneMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
779            boolean countOnly) {
780        // It's a bit tricker because it has to be consistent with
781        // updateMatchScoresBasedOnPhoneMatches().
782        final String phoneType = String.valueOf(mMimeTypeIdPhone);
783        final String sql =
784                " FROM " + Tables.PHONE_LOOKUP + " AS p1" +
785                " JOIN " + Tables.DATA + " AS d1 ON " +
786                        "(d1." + Data._ID + "=p1." + PhoneLookupColumns.DATA_ID + ")" +
787                " JOIN " + Tables.PHONE_LOOKUP + " AS p2 ON (p1." + PhoneLookupColumns.MIN_MATCH +
788                        "=p2." + PhoneLookupColumns.MIN_MATCH + ")" +
789                " JOIN " + Tables.DATA + " AS d2 ON " +
790                        "(d2." + Data._ID + "=p2." + PhoneLookupColumns.DATA_ID + ")" +
791                " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + phoneType +
792                " AND d2." + DataColumns.MIMETYPE_ID + " = " + phoneType +
793                " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
794                " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")" +
795                " AND PHONE_NUMBERS_EQUAL(d1." + Phone.NUMBER + ",d2." + Phone.NUMBER + "," +
796                        String.valueOf(mDbHelper.getUseStrictPhoneNumberComparisonParameter()) +
797                        ")";
798        return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
799                RawContactMatchingSelectionStatement.SELECT_ID + sql;
800    }
801
802    protected final String buildExceptionMatchingSql(String rawContactIdSet1,
803            String rawContactIdSet2) {
804        return "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " +
805                AggregationExceptions.RAW_CONTACT_ID2 +
806                " FROM " + Tables.AGGREGATION_EXCEPTIONS +
807                " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" +
808                rawContactIdSet1 + ")" +
809                " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" +
810                " AND " + AggregationExceptions.TYPE + "=" +
811                AggregationExceptions.TYPE_KEEP_TOGETHER ;
812    }
813
814    protected final boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query) {
815        return DatabaseUtils.longForQuery(db, query, null) > 0;
816    }
817
818    /**
819     * Partition the given raw contact Ids to connected component based on aggregation exception,
820     * identity matching, email matching or phone matching.
821     */
822    protected final Set<Set<Long>> findConnectedRawContacts(SQLiteDatabase db, Set<Long>
823            rawContactIdSet) {
824        // Connections between two raw contacts
825        final Multimap<Long, Long> matchingRawIdPairs = HashMultimap.create();
826        String rawContactIds = TextUtils.join(",", rawContactIdSet);
827        findIdPairs(db, buildExceptionMatchingSql(rawContactIds, rawContactIds),
828                matchingRawIdPairs);
829        findIdPairs(db, buildIdentityMatchingSql(rawContactIds, rawContactIds,
830                /* isIdentityMatching =*/ true, /* countOnly =*/false), matchingRawIdPairs);
831        findIdPairs(db, buildEmailMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false),
832                matchingRawIdPairs);
833        findIdPairs(db, buildPhoneMatchingSql(rawContactIds, rawContactIds,  /* countOnly =*/false),
834                matchingRawIdPairs);
835
836        return ContactAggregatorHelper.findConnectedComponents(rawContactIdSet, matchingRawIdPairs);
837    }
838
839    /**
840     * Given a query which will return two non-null IDs in the first two columns as results, this
841     * method will put two entries into the given result map for each pair of different IDs, one
842     * keyed by each ID.
843     */
844    protected final void findIdPairs(SQLiteDatabase db, String query,
845            Multimap<Long, Long> results) {
846        Cursor cursor = db.rawQuery(query, null);
847        try {
848            cursor.moveToPosition(-1);
849            while (cursor.moveToNext()) {
850                long idA = cursor.getLong(0);
851                long idB = cursor.getLong(1);
852                if (idA != idB) {
853                    results.put(idA, idB);
854                    results.put(idB, idA);
855                }
856            }
857        } finally {
858            cursor.close();
859        }
860    }
861
862    /**
863     * Creates a new Contact for a given set of the raw contacts of {@code rawContactIds} if the
864     * given contactId is null. Otherwise, regroup them into contact with {@code contactId}.
865     */
866    protected final void createContactForRawContacts(SQLiteDatabase db,
867            TransactionContext txContext, Set<Long> rawContactIds, Long contactId) {
868        if (rawContactIds.isEmpty()) {
869            // No raw contact id is provided.
870            return;
871        }
872
873        // If contactId is not provided, generates a new one.
874        if (contactId == null) {
875            mSelectionArgs1[0] = String.valueOf(rawContactIds.iterator().next());
876            computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
877                    mContactInsert);
878            contactId = mContactInsert.executeInsert();
879        }
880        for (Long rawContactId : rawContactIds) {
881            setContactIdAndMarkAggregated(rawContactId, contactId);
882            setPresenceContactId(rawContactId, contactId);
883        }
884        updateAggregateData(txContext, contactId);
885    }
886
887    protected static class RawContactIdQuery {
888        public static final String TABLE = Tables.RAW_CONTACTS;
889        public static final String[] COLUMNS = {RawContacts._ID, RawContactsColumns.ACCOUNT_ID };
890        public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
891        public static final int RAW_CONTACT_ID = 0;
892        public static final int ACCOUNT_ID = 1;
893    }
894
895    /**
896     * Updates the contact ID for the specified contact.
897     */
898    protected final void setContactId(long rawContactId, long contactId) {
899        mContactIdUpdate.bindLong(1, contactId);
900        mContactIdUpdate.bindLong(2, rawContactId);
901        mContactIdUpdate.execute();
902    }
903
904    /**
905     * Marks the list of raw contact IDs as aggregated.
906     *
907     * @param rawContactIds comma separated raw contact ids
908     */
909    protected final void markAggregated(SQLiteDatabase db, String rawContactIds) {
910        final String sql = "UPDATE " + Tables.RAW_CONTACTS +
911                " SET " + RawContactsColumns.AGGREGATION_NEEDED + "=0" +
912                " WHERE " + RawContacts._ID + " in (" + rawContactIds + ")";
913        db.execSQL(sql);
914    }
915
916    /**
917     * Updates the contact ID for the specified contact and marks the raw contact as aggregated.
918     */
919    private void setContactIdAndMarkAggregated(long rawContactId, long contactId) {
920        mContactIdAndMarkAggregatedUpdate.bindLong(1, contactId);
921        mContactIdAndMarkAggregatedUpdate.bindLong(2, rawContactId);
922        mContactIdAndMarkAggregatedUpdate.execute();
923    }
924
925    private void setPresenceContactId(long rawContactId, long contactId) {
926        mPresenceContactIdUpdate.bindLong(1, contactId);
927        mPresenceContactIdUpdate.bindLong(2, rawContactId);
928        mPresenceContactIdUpdate.execute();
929    }
930
931    private void unpinRawContact(long rawContactId) {
932        mResetPinnedForRawContact.bindLong(1, rawContactId);
933        mResetPinnedForRawContact.execute();
934    }
935
936    interface AggregateExceptionPrefetchQuery {
937        String TABLE = Tables.AGGREGATION_EXCEPTIONS;
938
939        String[] COLUMNS = {
940                AggregationExceptions.RAW_CONTACT_ID1,
941                AggregationExceptions.RAW_CONTACT_ID2,
942        };
943
944        int RAW_CONTACT_ID1 = 0;
945        int RAW_CONTACT_ID2 = 1;
946    }
947
948    // A set of raw contact IDs for which there are aggregation exceptions
949    protected final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>();
950    protected boolean mAggregationExceptionIdsValid;
951
952    public final void invalidateAggregationExceptionCache() {
953        mAggregationExceptionIdsValid = false;
954    }
955
956    /**
957     * Finds all raw contact IDs for which there are aggregation exceptions. The list of
958     * ids is used as an optimization in aggregation: there is no point to run a query against
959     * the agg_exceptions table if it is known that there are no records there for a given
960     * raw contact ID.
961     */
962    protected final void prefetchAggregationExceptionIds(SQLiteDatabase db) {
963        mAggregationExceptionIds.clear();
964        final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE,
965                AggregateExceptionPrefetchQuery.COLUMNS,
966                null, null, null, null, null);
967
968        try {
969            while (c.moveToNext()) {
970                long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1);
971                long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2);
972                mAggregationExceptionIds.add(rawContactId1);
973                mAggregationExceptionIds.add(rawContactId2);
974            }
975        } finally {
976            c.close();
977        }
978
979        mAggregationExceptionIdsValid = true;
980    }
981
982    protected interface NameLookupQuery {
983        String TABLE = Tables.NAME_LOOKUP;
984
985        String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?";
986        String SELECTION_STRUCTURED_NAME_BASED =
987                SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL;
988
989        String[] COLUMNS = new String[] {
990                NameLookupColumns.NORMALIZED_NAME,
991                NameLookupColumns.NAME_TYPE
992        };
993
994        int NORMALIZED_NAME = 0;
995        int NAME_TYPE = 1;
996    }
997
998    protected final void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId,
999            MatchCandidateList candidates, boolean structuredNameBased) {
1000        candidates.clear();
1001        mSelectionArgs1[0] = String.valueOf(rawContactId);
1002        Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
1003                structuredNameBased
1004                        ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED
1005                        : NameLookupQuery.SELECTION,
1006                mSelectionArgs1, null, null, null);
1007        try {
1008            while (c.moveToNext()) {
1009                String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME);
1010                int type = c.getInt(NameLookupQuery.NAME_TYPE);
1011                candidates.add(normalizedName, type);
1012            }
1013        } finally {
1014            c.close();
1015        }
1016    }
1017
1018    interface AggregateExceptionQuery {
1019        String TABLE = Tables.AGGREGATION_EXCEPTIONS
1020                + " JOIN raw_contacts raw_contacts1 "
1021                + " ON (agg_exceptions.raw_contact_id1 = raw_contacts1._id) "
1022                + " JOIN raw_contacts raw_contacts2 "
1023                + " ON (agg_exceptions.raw_contact_id2 = raw_contacts2._id) ";
1024
1025        String[] COLUMNS = {
1026                AggregationExceptions.TYPE,
1027                AggregationExceptions.RAW_CONTACT_ID1,
1028                "raw_contacts1." + RawContacts.CONTACT_ID,
1029                "raw_contacts1." + RawContactsColumns.ACCOUNT_ID,
1030                "raw_contacts1." + RawContactsColumns.AGGREGATION_NEEDED,
1031                AggregationExceptions.RAW_CONTACT_ID2,
1032                "raw_contacts2." + RawContacts.CONTACT_ID,
1033                "raw_contacts2." + RawContactsColumns.ACCOUNT_ID,
1034                "raw_contacts2." + RawContactsColumns.AGGREGATION_NEEDED,
1035        };
1036
1037        int TYPE = 0;
1038        int RAW_CONTACT_ID1 = 1;
1039        int CONTACT_ID1 = 2;
1040        int ACCOUNT_ID1 = 3;
1041        int AGGREGATION_NEEDED_1 = 4;
1042        int RAW_CONTACT_ID2 = 5;
1043        int CONTACT_ID2 = 6;
1044        int ACCOUNT_ID2 = 7;
1045        int AGGREGATION_NEEDED_2 = 8;
1046    }
1047
1048    protected interface NameLookupMatchQueryWithParameter {
1049        String TABLE = Tables.NAME_LOOKUP
1050                + " JOIN " + Tables.RAW_CONTACTS +
1051                " ON (" + NameLookupColumns.RAW_CONTACT_ID + " = "
1052                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1053
1054        String[] COLUMNS = new String[] {
1055                RawContacts._ID,
1056                RawContacts.CONTACT_ID,
1057                RawContactsColumns.ACCOUNT_ID,
1058                NameLookupColumns.NORMALIZED_NAME,
1059                NameLookupColumns.NAME_TYPE,
1060        };
1061
1062        int RAW_CONTACT_ID = 0;
1063        int CONTACT_ID = 1;
1064        int ACCOUNT_ID = 2;
1065        int NAME = 3;
1066        int NAME_TYPE = 4;
1067    }
1068
1069    protected final class NameLookupSelectionBuilder extends NameLookupBuilder {
1070
1071        private final MatchCandidateList mNameLookupCandidates;
1072
1073        private StringBuilder mSelection = new StringBuilder(
1074                NameLookupColumns.NORMALIZED_NAME + " IN(");
1075
1076
1077        public NameLookupSelectionBuilder(NameSplitter splitter, MatchCandidateList candidates) {
1078            super(splitter);
1079            this.mNameLookupCandidates = candidates;
1080        }
1081
1082        @Override
1083        protected String[] getCommonNicknameClusters(String normalizedName) {
1084            return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
1085        }
1086
1087        @Override
1088        protected void insertNameLookup(
1089                long rawContactId, long dataId, int lookupType, String string) {
1090            mNameLookupCandidates.add(string, lookupType);
1091            DatabaseUtils.appendEscapedSQLString(mSelection, string);
1092            mSelection.append(',');
1093        }
1094
1095        public boolean isEmpty() {
1096            return mNameLookupCandidates.isEmpty();
1097        }
1098
1099        public String getSelection() {
1100            mSelection.setLength(mSelection.length() - 1);      // Strip last comma
1101            mSelection.append(')');
1102            return mSelection.toString();
1103        }
1104
1105        public int getLookupType(String name) {
1106            for (int i = 0; i < mNameLookupCandidates.mCount; i++) {
1107                if (mNameLookupCandidates.mList.get(i).mName.equals(name)) {
1108                    return mNameLookupCandidates.mList.get(i).mLookupType;
1109                }
1110            }
1111            throw new IllegalStateException();
1112        }
1113    }
1114
1115    /**
1116     * Finds contacts with names matching the specified name.
1117     */
1118    protected final void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query,
1119            MatchCandidateList candidates, ContactMatcher matcher) {
1120        candidates.clear();
1121        NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder(
1122                mNameSplitter, candidates);
1123        builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED);
1124        if (builder.isEmpty()) {
1125            return;
1126        }
1127
1128        Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE,
1129                NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null,
1130                null, PRIMARY_HIT_LIMIT_STRING);
1131        try {
1132            while (c.moveToNext()) {
1133                long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID);
1134                String name = c.getString(NameLookupMatchQueryWithParameter.NAME);
1135                int nameTypeA = builder.getLookupType(name);
1136                int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE);
1137                matcher.matchName(contactId, nameTypeA, name, nameTypeB, name,
1138                        ContactMatcher.MATCHING_ALGORITHM_EXACT);
1139                if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) {
1140                    matcher.updateScoreWithNicknameMatch(contactId);
1141                }
1142            }
1143        } finally {
1144            c.close();
1145        }
1146    }
1147
1148    protected interface EmailLookupQuery {
1149        String TABLE = Tables.DATA + " dataA"
1150                + " JOIN " + Tables.DATA + " dataB" +
1151                " ON dataA." + Email.DATA + "= dataB." + Email.DATA
1152                + " JOIN " + Tables.RAW_CONTACTS +
1153                " ON (dataB." + Data.RAW_CONTACT_ID + " = "
1154                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1155
1156        String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
1157                + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
1158                + " AND dataA." + Email.DATA + " NOT NULL"
1159                + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
1160                + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
1161                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1162
1163        String[] COLUMNS = new String[] {
1164                Tables.RAW_CONTACTS + "." + RawContacts._ID,
1165                RawContacts.CONTACT_ID,
1166                RawContactsColumns.ACCOUNT_ID
1167        };
1168
1169        int RAW_CONTACT_ID = 0;
1170        int CONTACT_ID = 1;
1171        int ACCOUNT_ID = 2;
1172    }
1173
1174    protected interface PhoneLookupQuery {
1175        String TABLE = Tables.PHONE_LOOKUP + " phoneA"
1176                + " JOIN " + Tables.DATA + " dataA"
1177                + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
1178                + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
1179                + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
1180                        + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
1181                + " JOIN " + Tables.DATA + " dataB"
1182                + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
1183                + " JOIN " + Tables.RAW_CONTACTS
1184                + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
1185                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1186
1187        String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
1188                + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
1189                        + "dataB." + Phone.NUMBER + ",?)"
1190                + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0"
1191                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1192
1193        String[] COLUMNS = new String[] {
1194                Tables.RAW_CONTACTS + "." + RawContacts._ID,
1195                RawContacts.CONTACT_ID,
1196                RawContactsColumns.ACCOUNT_ID
1197        };
1198
1199        int RAW_CONTACT_ID = 0;
1200        int CONTACT_ID = 1;
1201        int ACCOUNT_ID = 2;
1202    }
1203
1204    private interface ContactNameLookupQuery {
1205        String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
1206
1207        String[] COLUMNS = new String[]{
1208                RawContacts.CONTACT_ID,
1209                NameLookupColumns.NORMALIZED_NAME,
1210                NameLookupColumns.NAME_TYPE
1211        };
1212
1213        int CONTACT_ID = 0;
1214        int NORMALIZED_NAME = 1;
1215        int NAME_TYPE = 2;
1216    }
1217
1218    /**
1219     * Loads all candidate rows from the name lookup table and updates match scores based
1220     * on that data.
1221     */
1222    private void matchAllCandidates(SQLiteDatabase db, String selection,
1223            MatchCandidateList candidates, ContactMatcher matcher, int algorithm, String limit) {
1224        final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
1225                selection, null, null, null, null, limit);
1226
1227        try {
1228            while (c.moveToNext()) {
1229                Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
1230                String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
1231                int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
1232
1233                // Note the N^2 complexity of the following fragment. This is not a huge concern
1234                // since the number of candidates is very small and in general secondary hits
1235                // in the absence of primary hits are rare.
1236                for (int i = 0; i < candidates.mCount; i++) {
1237                    NameMatchCandidate candidate = candidates.mList.get(i);
1238                    matcher.matchName(contactId, candidate.mLookupType, candidate.mName,
1239                            nameType, name, algorithm);
1240                }
1241            }
1242        } finally {
1243            c.close();
1244        }
1245    }
1246
1247    private interface RawContactsQuery {
1248        String SQL_FORMAT_HAS_SUPER_PRIMARY_NAME =
1249                " EXISTS(SELECT 1 " +
1250                        " FROM " + Tables.DATA + " d " +
1251                        " WHERE d." + DataColumns.MIMETYPE_ID + "=%d " +
1252                        " AND d." + Data.RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID +
1253                        " AND d." + Data.IS_SUPER_PRIMARY + "=1)";
1254
1255        String SQL_FORMAT =
1256                "SELECT "
1257                        + RawContactsColumns.CONCRETE_ID + ","
1258                        + RawContactsColumns.DISPLAY_NAME + ","
1259                        + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
1260                        + AccountsColumns.CONCRETE_ACCOUNT_TYPE + ","
1261                        + AccountsColumns.CONCRETE_ACCOUNT_NAME + ","
1262                        + AccountsColumns.CONCRETE_DATA_SET + ","
1263                        + RawContacts.SOURCE_ID + ","
1264                        + RawContacts.CUSTOM_RINGTONE + ","
1265                        + RawContacts.SEND_TO_VOICEMAIL + ","
1266                        + RawContacts.RAW_LAST_TIME_CONTACTED + ","
1267                        + RawContacts.RAW_TIMES_CONTACTED + ","
1268                        + RawContacts.STARRED + ","
1269                        + RawContacts.PINNED + ","
1270                        + DataColumns.CONCRETE_ID + ","
1271                        + DataColumns.CONCRETE_MIMETYPE_ID + ","
1272                        + Data.IS_SUPER_PRIMARY + ","
1273                        + Photo.PHOTO_FILE_ID + ","
1274                        + SQL_FORMAT_HAS_SUPER_PRIMARY_NAME +
1275                " FROM " + Tables.RAW_CONTACTS +
1276                " JOIN " + Tables.ACCOUNTS + " ON ("
1277                    + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
1278                    + ")" +
1279                " LEFT OUTER JOIN " + Tables.DATA +
1280                " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
1281                        + " AND ((" + DataColumns.MIMETYPE_ID + "=%d"
1282                                + " AND " + Photo.PHOTO + " NOT NULL)"
1283                        + " OR (" + DataColumns.MIMETYPE_ID + "=%d"
1284                                + " AND " + Phone.NUMBER + " NOT NULL)))";
1285
1286        String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT +
1287                " WHERE " + RawContactsColumns.CONCRETE_ID + "=?";
1288
1289        String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT +
1290                " WHERE " + RawContacts.CONTACT_ID + "=?"
1291                + " AND " + RawContacts.DELETED + "=0";
1292
1293        int RAW_CONTACT_ID = 0;
1294        int DISPLAY_NAME = 1;
1295        int DISPLAY_NAME_SOURCE = 2;
1296        int ACCOUNT_TYPE = 3;
1297        int ACCOUNT_NAME = 4;
1298        int DATA_SET = 5;
1299        int SOURCE_ID = 6;
1300        int CUSTOM_RINGTONE = 7;
1301        int SEND_TO_VOICEMAIL = 8;
1302        int RAW_LAST_TIME_CONTACTED = 9;
1303        int RAW_TIMES_CONTACTED = 10;
1304        int STARRED = 11;
1305        int PINNED = 12;
1306        int DATA_ID = 13;
1307        int MIMETYPE_ID = 14;
1308        int IS_SUPER_PRIMARY = 15;
1309        int PHOTO_FILE_ID = 16;
1310        int HAS_SUPER_PRIMARY_NAME = 17;
1311    }
1312
1313    protected interface ContactReplaceSqlStatement {
1314        String UPDATE_SQL =
1315                "UPDATE " + Tables.CONTACTS +
1316                " SET "
1317                        + Contacts.NAME_RAW_CONTACT_ID + "=?, "
1318                        + Contacts.PHOTO_ID + "=?, "
1319                        + Contacts.PHOTO_FILE_ID + "=?, "
1320                        + Contacts.SEND_TO_VOICEMAIL + "=?, "
1321                        + Contacts.CUSTOM_RINGTONE + "=?, "
1322                        + Contacts.RAW_LAST_TIME_CONTACTED + "=?, "
1323                        + Contacts.RAW_TIMES_CONTACTED + "=?, "
1324                        + Contacts.STARRED + "=?, "
1325                        + Contacts.PINNED + "=?, "
1326                        + Contacts.HAS_PHONE_NUMBER + "=?, "
1327                        + Contacts.LOOKUP_KEY + "=?, "
1328                        + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=? " +
1329                " WHERE " + Contacts._ID + "=?";
1330
1331        String INSERT_SQL =
1332                "INSERT INTO " + Tables.CONTACTS + " ("
1333                        + Contacts.NAME_RAW_CONTACT_ID + ", "
1334                        + Contacts.PHOTO_ID + ", "
1335                        + Contacts.PHOTO_FILE_ID + ", "
1336                        + Contacts.SEND_TO_VOICEMAIL + ", "
1337                        + Contacts.CUSTOM_RINGTONE + ", "
1338                        + Contacts.RAW_LAST_TIME_CONTACTED + ", "
1339                        + Contacts.RAW_TIMES_CONTACTED + ", "
1340                        + Contacts.STARRED + ", "
1341                        + Contacts.PINNED + ", "
1342                        + Contacts.HAS_PHONE_NUMBER + ", "
1343                        + Contacts.LOOKUP_KEY + ", "
1344                        + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
1345                        + ") " +
1346                " VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";
1347
1348        int NAME_RAW_CONTACT_ID = 1;
1349        int PHOTO_ID = 2;
1350        int PHOTO_FILE_ID = 3;
1351        int SEND_TO_VOICEMAIL = 4;
1352        int CUSTOM_RINGTONE = 5;
1353        int RAW_LAST_TIME_CONTACTED = 6;
1354        int RAW_TIMES_CONTACTED = 7;
1355        int STARRED = 8;
1356        int PINNED = 9;
1357        int HAS_PHONE_NUMBER = 10;
1358        int LOOKUP_KEY = 11;
1359        int CONTACT_LAST_UPDATED_TIMESTAMP = 12;
1360        int CONTACT_ID = 13;
1361    }
1362
1363    /**
1364     * Computes aggregate-level data for the specified aggregate contact ID.
1365     */
1366    protected void computeAggregateData(SQLiteDatabase db, long contactId,
1367            SQLiteStatement statement) {
1368        mSelectionArgs1[0] = String.valueOf(contactId);
1369        computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement);
1370    }
1371
1372    /**
1373     * Indicates whether the given photo entry and priority gives this photo a higher overall
1374     * priority than the current best photo entry and priority.
1375     */
1376    private boolean hasHigherPhotoPriority(PhotoEntry photoEntry, int priority,
1377            PhotoEntry bestPhotoEntry, int bestPriority) {
1378        int photoComparison = photoEntry.compareTo(bestPhotoEntry);
1379        return photoComparison < 0 || photoComparison == 0 && priority > bestPriority;
1380    }
1381
1382    /**
1383     * Computes aggregate-level data from constituent raw contacts.
1384     */
1385    protected final void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs,
1386            SQLiteStatement statement) {
1387        long currentRawContactId = -1;
1388        long bestPhotoId = -1;
1389        long bestPhotoFileId = 0;
1390        PhotoEntry bestPhotoEntry = null;
1391        boolean foundSuperPrimaryPhoto = false;
1392        int photoPriority = -1;
1393        int totalRowCount = 0;
1394        int contactSendToVoicemail = 0;
1395        String contactCustomRingtone = null;
1396        long contactLastTimeContacted = 0;
1397        int contactTimesContacted = 0;
1398        int contactStarred = 0;
1399        int contactPinned = Integer.MAX_VALUE;
1400        int hasPhoneNumber = 0;
1401        StringBuilder lookupKey = new StringBuilder();
1402
1403        mDisplayNameCandidate.clear();
1404
1405        Cursor c = db.rawQuery(sql, sqlArgs);
1406        try {
1407            while (c.moveToNext()) {
1408                long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
1409                if (rawContactId != currentRawContactId) {
1410                    currentRawContactId = rawContactId;
1411                    totalRowCount++;
1412
1413                    // Assemble sub-account.
1414                    String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
1415                    String dataSet = c.getString(RawContactsQuery.DATA_SET);
1416                    String accountWithDataSet = (!TextUtils.isEmpty(dataSet))
1417                            ? accountType + "/" + dataSet
1418                            : accountType;
1419
1420                    // Display name
1421                    String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
1422                    int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE);
1423                    int isNameSuperPrimary = c.getInt(RawContactsQuery.HAS_SUPER_PRIMARY_NAME);
1424                    processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
1425                            mContactsProvider.isWritableAccountWithDataSet(accountWithDataSet),
1426                            isNameSuperPrimary != 0);
1427
1428                    // Contact options
1429                    if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
1430                        boolean sendToVoicemail =
1431                                (c.getInt(RawContactsQuery.SEND_TO_VOICEMAIL) != 0);
1432                        if (sendToVoicemail) {
1433                            contactSendToVoicemail++;
1434                        }
1435                    }
1436
1437                    if (contactCustomRingtone == null
1438                            && !c.isNull(RawContactsQuery.CUSTOM_RINGTONE)) {
1439                        contactCustomRingtone = c.getString(RawContactsQuery.CUSTOM_RINGTONE);
1440                    }
1441
1442                    long lastTimeContacted = c.getLong(RawContactsQuery.RAW_LAST_TIME_CONTACTED);
1443                    if (lastTimeContacted > contactLastTimeContacted) {
1444                        contactLastTimeContacted = lastTimeContacted;
1445                    }
1446
1447                    int timesContacted = c.getInt(RawContactsQuery.RAW_TIMES_CONTACTED);
1448                    if (timesContacted > contactTimesContacted) {
1449                        contactTimesContacted = timesContacted;
1450                    }
1451
1452                    if (c.getInt(RawContactsQuery.STARRED) != 0) {
1453                        contactStarred = 1;
1454                    }
1455
1456                    // contactPinned should be the lowest value of its constituent raw contacts,
1457                    // excluding negative integers
1458                    final int rawContactPinned = c.getInt(RawContactsQuery.PINNED);
1459                    if (rawContactPinned > PinnedPositions.UNPINNED) {
1460                        contactPinned = Math.min(contactPinned, rawContactPinned);
1461                    }
1462
1463                    appendLookupKey(
1464                            lookupKey,
1465                            accountWithDataSet,
1466                            c.getString(RawContactsQuery.ACCOUNT_NAME),
1467                            rawContactId,
1468                            c.getString(RawContactsQuery.SOURCE_ID),
1469                            displayName);
1470                }
1471
1472                if (!c.isNull(RawContactsQuery.DATA_ID)) {
1473                    long dataId = c.getLong(RawContactsQuery.DATA_ID);
1474                    long photoFileId = c.getLong(RawContactsQuery.PHOTO_FILE_ID);
1475                    int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
1476                    boolean superPrimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0;
1477                    if (mimetypeId == mMimeTypeIdPhoto) {
1478                        if (!foundSuperPrimaryPhoto) {
1479                            // Lookup the metadata for the photo, if available.  Note that data set
1480                            // does not come into play here, since accounts are looked up in the
1481                            // account manager in the priority resolver.
1482                            PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
1483                            String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
1484                            int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
1485                            if (superPrimary || hasHigherPhotoPriority(
1486                                    photoEntry, priority, bestPhotoEntry, photoPriority)) {
1487                                bestPhotoEntry = photoEntry;
1488                                photoPriority = priority;
1489                                bestPhotoId = dataId;
1490                                bestPhotoFileId = photoFileId;
1491                                foundSuperPrimaryPhoto |= superPrimary;
1492                            }
1493                        }
1494                    } else if (mimetypeId == mMimeTypeIdPhone) {
1495                        hasPhoneNumber = 1;
1496                    }
1497                }
1498            }
1499        } finally {
1500            c.close();
1501        }
1502
1503        if (contactPinned == Integer.MAX_VALUE) {
1504            contactPinned = PinnedPositions.UNPINNED;
1505        }
1506
1507        statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID,
1508                mDisplayNameCandidate.rawContactId);
1509
1510        if (bestPhotoId != -1) {
1511            statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId);
1512        } else {
1513            statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID);
1514        }
1515
1516        if (bestPhotoFileId != 0) {
1517            statement.bindLong(ContactReplaceSqlStatement.PHOTO_FILE_ID, bestPhotoFileId);
1518        } else {
1519            statement.bindNull(ContactReplaceSqlStatement.PHOTO_FILE_ID);
1520        }
1521
1522        statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL,
1523                totalRowCount == contactSendToVoicemail ? 1 : 0);
1524        DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
1525                contactCustomRingtone);
1526        statement.bindLong(ContactReplaceSqlStatement.RAW_LAST_TIME_CONTACTED,
1527                contactLastTimeContacted);
1528        statement.bindLong(ContactReplaceSqlStatement.RAW_TIMES_CONTACTED,
1529                contactTimesContacted);
1530        statement.bindLong(ContactReplaceSqlStatement.STARRED,
1531                contactStarred);
1532        statement.bindLong(ContactReplaceSqlStatement.PINNED,
1533                contactPinned);
1534        statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER,
1535                hasPhoneNumber);
1536        statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
1537                Uri.encode(lookupKey.toString()));
1538        statement.bindLong(ContactReplaceSqlStatement.CONTACT_LAST_UPDATED_TIMESTAMP,
1539                Clock.getInstance().currentTimeMillis());
1540    }
1541
1542    /**
1543     * Builds a lookup key using the given data.
1544     */
1545    // Overridden by ProfileAggregator.
1546    protected void appendLookupKey(StringBuilder sb, String accountTypeWithDataSet,
1547            String accountName, long rawContactId, String sourceId, String displayName) {
1548        ContactLookupKey.appendToLookupKey(sb, accountTypeWithDataSet, accountName, rawContactId,
1549                sourceId, displayName);
1550    }
1551
1552    /**
1553     * Uses the supplied values to determine if they represent a "better" display name
1554     * for the aggregate contact currently evaluated.  If so, it updates
1555     * {@link #mDisplayNameCandidate} with the new values.
1556     */
1557    private void processDisplayNameCandidate(long rawContactId, String displayName,
1558            int displayNameSource, boolean writableAccount, boolean isNameSuperPrimary) {
1559
1560        boolean replace = false;
1561        if (mDisplayNameCandidate.rawContactId == -1) {
1562            // No previous values available
1563            replace = true;
1564        } else if (!TextUtils.isEmpty(displayName)) {
1565            if (isNameSuperPrimary) {
1566                // A super primary name is better than any other name
1567                replace = true;
1568            } else if (mDisplayNameCandidate.isNameSuperPrimary == isNameSuperPrimary) {
1569                if (mDisplayNameCandidate.displayNameSource < displayNameSource) {
1570                    // New values come from an superior source, e.g. structured name vs phone number
1571                    replace = true;
1572                } else if (mDisplayNameCandidate.displayNameSource == displayNameSource) {
1573                    if (!mDisplayNameCandidate.writableAccount && writableAccount) {
1574                        replace = true;
1575                    } else if (mDisplayNameCandidate.writableAccount == writableAccount) {
1576                        if (NameNormalizer.compareComplexity(displayName,
1577                                mDisplayNameCandidate.displayName) > 0) {
1578                            // New name is more complex than the previously found one
1579                            replace = true;
1580                        }
1581                    }
1582                }
1583            }
1584        }
1585
1586        if (replace) {
1587            mDisplayNameCandidate.rawContactId = rawContactId;
1588            mDisplayNameCandidate.displayName = displayName;
1589            mDisplayNameCandidate.displayNameSource = displayNameSource;
1590            mDisplayNameCandidate.isNameSuperPrimary = isNameSuperPrimary;
1591            mDisplayNameCandidate.writableAccount = writableAccount;
1592        }
1593    }
1594
1595    private interface PhotoIdQuery {
1596        final String[] COLUMNS = new String[] {
1597            AccountsColumns.CONCRETE_ACCOUNT_TYPE,
1598            DataColumns.CONCRETE_ID,
1599            Data.IS_SUPER_PRIMARY,
1600            Photo.PHOTO_FILE_ID,
1601        };
1602
1603        int ACCOUNT_TYPE = 0;
1604        int DATA_ID = 1;
1605        int IS_SUPER_PRIMARY = 2;
1606        int PHOTO_FILE_ID = 3;
1607    }
1608
1609    public final void updatePhotoId(SQLiteDatabase db, long rawContactId) {
1610
1611        long contactId = mDbHelper.getContactId(rawContactId);
1612        if (contactId == 0) {
1613            return;
1614        }
1615
1616        long bestPhotoId = -1;
1617        long bestPhotoFileId = 0;
1618        int photoPriority = -1;
1619
1620        long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
1621
1622        String tables = Tables.RAW_CONTACTS
1623                + " JOIN " + Tables.ACCOUNTS + " ON ("
1624                    + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
1625                    + ")"
1626                + " JOIN " + Tables.DATA + " ON("
1627                + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
1628                + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
1629                        + Photo.PHOTO + " NOT NULL))";
1630
1631        mSelectionArgs1[0] = String.valueOf(contactId);
1632        final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS,
1633                RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
1634        try {
1635            PhotoEntry bestPhotoEntry = null;
1636            while (c.moveToNext()) {
1637                long dataId = c.getLong(PhotoIdQuery.DATA_ID);
1638                long photoFileId = c.getLong(PhotoIdQuery.PHOTO_FILE_ID);
1639                boolean superPrimary = c.getInt(PhotoIdQuery.IS_SUPER_PRIMARY) != 0;
1640                PhotoEntry photoEntry = getPhotoMetadata(db, photoFileId);
1641
1642                // Note that data set does not come into play here, since accounts are looked up in
1643                // the account manager in the priority resolver.
1644                String accountType = c.getString(PhotoIdQuery.ACCOUNT_TYPE);
1645                int priority = mPhotoPriorityResolver.getPhotoPriority(accountType);
1646                if (superPrimary || hasHigherPhotoPriority(
1647                        photoEntry, priority, bestPhotoEntry, photoPriority)) {
1648                    bestPhotoEntry = photoEntry;
1649                    photoPriority = priority;
1650                    bestPhotoId = dataId;
1651                    bestPhotoFileId = photoFileId;
1652                    if (superPrimary) {
1653                        break;
1654                    }
1655                }
1656            }
1657        } finally {
1658            c.close();
1659        }
1660
1661        if (bestPhotoId == -1) {
1662            mPhotoIdUpdate.bindNull(1);
1663        } else {
1664            mPhotoIdUpdate.bindLong(1, bestPhotoId);
1665        }
1666
1667        if (bestPhotoFileId == 0) {
1668            mPhotoIdUpdate.bindNull(2);
1669        } else {
1670            mPhotoIdUpdate.bindLong(2, bestPhotoFileId);
1671        }
1672
1673        mPhotoIdUpdate.bindLong(3, contactId);
1674        mPhotoIdUpdate.execute();
1675    }
1676
1677    private interface PhotoFileQuery {
1678        final String[] COLUMNS = new String[] {
1679                PhotoFiles.HEIGHT,
1680                PhotoFiles.WIDTH,
1681                PhotoFiles.FILESIZE
1682        };
1683
1684        int HEIGHT = 0;
1685        int WIDTH = 1;
1686        int FILESIZE = 2;
1687    }
1688
1689    private class PhotoEntry implements Comparable<PhotoEntry> {
1690        // Pixel count (width * height) for the image.
1691        final int pixelCount;
1692
1693        // File size (in bytes) of the image.  Not populated if the image is a thumbnail.
1694        final int fileSize;
1695
1696        private PhotoEntry(int pixelCount, int fileSize) {
1697            this.pixelCount = pixelCount;
1698            this.fileSize = fileSize;
1699        }
1700
1701        @Override
1702        public int compareTo(PhotoEntry pe) {
1703            if (pe == null) {
1704                return -1;
1705            }
1706            if (pixelCount == pe.pixelCount) {
1707                return pe.fileSize - fileSize;
1708            } else {
1709                return pe.pixelCount - pixelCount;
1710            }
1711        }
1712    }
1713
1714    private PhotoEntry getPhotoMetadata(SQLiteDatabase db, long photoFileId) {
1715        if (photoFileId == 0) {
1716            // Assume standard thumbnail size.  Don't bother getting a file size for priority;
1717            // we should fall back to photo priority resolver if all we have are thumbnails.
1718            int thumbDim = mContactsProvider.getMaxThumbnailDim();
1719            return new PhotoEntry(thumbDim * thumbDim, 0);
1720        } else {
1721            Cursor c = db.query(Tables.PHOTO_FILES, PhotoFileQuery.COLUMNS, PhotoFiles._ID + "=?",
1722                    new String[]{String.valueOf(photoFileId)}, null, null, null);
1723            try {
1724                if (c.getCount() == 1) {
1725                    c.moveToFirst();
1726                    int pixelCount =
1727                            c.getInt(PhotoFileQuery.HEIGHT) * c.getInt(PhotoFileQuery.WIDTH);
1728                    return new PhotoEntry(pixelCount, c.getInt(PhotoFileQuery.FILESIZE));
1729                }
1730            } finally {
1731                c.close();
1732            }
1733        }
1734        return new PhotoEntry(0, 0);
1735    }
1736
1737    private interface DisplayNameQuery {
1738        String SQL_HAS_SUPER_PRIMARY_NAME =
1739                " EXISTS(SELECT 1 " +
1740                        " FROM " + Tables.DATA + " d " +
1741                        " WHERE d." + DataColumns.MIMETYPE_ID + "=? " +
1742                        " AND d." + Data.RAW_CONTACT_ID + "=" + Views.RAW_CONTACTS
1743                        + "." + RawContacts._ID +
1744                        " AND d." + Data.IS_SUPER_PRIMARY + "=1)";
1745
1746        String SQL =
1747                "SELECT "
1748                        + RawContacts._ID + ","
1749                        + RawContactsColumns.DISPLAY_NAME + ","
1750                        + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
1751                        + SQL_HAS_SUPER_PRIMARY_NAME + ","
1752                        + RawContacts.SOURCE_ID + ","
1753                        + RawContacts.ACCOUNT_TYPE_AND_DATA_SET +
1754                " FROM " + Views.RAW_CONTACTS +
1755                " WHERE " + RawContacts.CONTACT_ID + "=? ";
1756
1757        int _ID = 0;
1758        int DISPLAY_NAME = 1;
1759        int DISPLAY_NAME_SOURCE = 2;
1760        int HAS_SUPER_PRIMARY_NAME = 3;
1761        int SOURCE_ID = 4;
1762        int ACCOUNT_TYPE_AND_DATA_SET = 5;
1763    }
1764
1765    public final void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) {
1766        long contactId = mDbHelper.getContactId(rawContactId);
1767        if (contactId == 0) {
1768            return;
1769        }
1770
1771        updateDisplayNameForContact(db, contactId);
1772    }
1773
1774    public final void updateDisplayNameForContact(SQLiteDatabase db, long contactId) {
1775        boolean lookupKeyUpdateNeeded = false;
1776
1777        mDisplayNameCandidate.clear();
1778
1779        mSelectionArgs2[0] = String.valueOf(mDbHelper.getMimeTypeIdForStructuredName());
1780        mSelectionArgs2[1] = String.valueOf(contactId);
1781        final Cursor c = db.rawQuery(DisplayNameQuery.SQL, mSelectionArgs2);
1782        try {
1783            while (c.moveToNext()) {
1784                long rawContactId = c.getLong(DisplayNameQuery._ID);
1785                String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME);
1786                int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE);
1787                int isNameSuperPrimary = c.getInt(DisplayNameQuery.HAS_SUPER_PRIMARY_NAME);
1788                String accountTypeAndDataSet = c.getString(
1789                        DisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET);
1790                processDisplayNameCandidate(rawContactId, displayName, displayNameSource,
1791                        mContactsProvider.isWritableAccountWithDataSet(accountTypeAndDataSet),
1792                        isNameSuperPrimary != 0);
1793
1794                // If the raw contact has no source id, the lookup key is based on the display
1795                // name, so the lookup key needs to be updated.
1796                lookupKeyUpdateNeeded |= c.isNull(DisplayNameQuery.SOURCE_ID);
1797            }
1798        } finally {
1799            c.close();
1800        }
1801
1802        if (mDisplayNameCandidate.rawContactId != -1) {
1803            mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId);
1804            mDisplayNameUpdate.bindLong(2, contactId);
1805            mDisplayNameUpdate.execute();
1806        }
1807
1808        if (lookupKeyUpdateNeeded) {
1809            updateLookupKeyForContact(db, contactId);
1810        }
1811    }
1812
1813
1814    /**
1815     * Updates the {@link Contacts#HAS_PHONE_NUMBER} flag for the aggregate contact containing the
1816     * specified raw contact.
1817     */
1818    public final void updateHasPhoneNumber(SQLiteDatabase db, long rawContactId) {
1819
1820        long contactId = mDbHelper.getContactId(rawContactId);
1821        if (contactId == 0) {
1822            return;
1823        }
1824
1825        final SQLiteStatement hasPhoneNumberUpdate = db.compileStatement(
1826                "UPDATE " + Tables.CONTACTS +
1827                " SET " + Contacts.HAS_PHONE_NUMBER + "="
1828                        + "(SELECT (CASE WHEN COUNT(*)=0 THEN 0 ELSE 1 END)"
1829                        + " FROM " + Tables.DATA_JOIN_RAW_CONTACTS
1830                        + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
1831                                + " AND " + Phone.NUMBER + " NOT NULL"
1832                                + " AND " + RawContacts.CONTACT_ID + "=?)" +
1833                " WHERE " + Contacts._ID + "=?");
1834        try {
1835            hasPhoneNumberUpdate.bindLong(1, mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE));
1836            hasPhoneNumberUpdate.bindLong(2, contactId);
1837            hasPhoneNumberUpdate.bindLong(3, contactId);
1838            hasPhoneNumberUpdate.execute();
1839        } finally {
1840            hasPhoneNumberUpdate.close();
1841        }
1842    }
1843
1844    private interface LookupKeyQuery {
1845        String TABLE = Views.RAW_CONTACTS;
1846        String[] COLUMNS = new String[] {
1847            RawContacts._ID,
1848            RawContactsColumns.DISPLAY_NAME,
1849            RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
1850            RawContacts.ACCOUNT_NAME,
1851            RawContacts.SOURCE_ID,
1852        };
1853
1854        int ID = 0;
1855        int DISPLAY_NAME = 1;
1856        int ACCOUNT_TYPE_AND_DATA_SET = 2;
1857        int ACCOUNT_NAME = 3;
1858        int SOURCE_ID = 4;
1859    }
1860
1861    public final void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
1862        long contactId = mDbHelper.getContactId(rawContactId);
1863        if (contactId == 0) {
1864            return;
1865        }
1866
1867        updateLookupKeyForContact(db, contactId);
1868    }
1869
1870    private void updateLookupKeyForContact(SQLiteDatabase db, long contactId) {
1871        String lookupKey = computeLookupKeyForContact(db, contactId);
1872
1873        if (lookupKey == null) {
1874            mLookupKeyUpdate.bindNull(1);
1875        } else {
1876            mLookupKeyUpdate.bindString(1, Uri.encode(lookupKey));
1877        }
1878        mLookupKeyUpdate.bindLong(2, contactId);
1879
1880        mLookupKeyUpdate.execute();
1881    }
1882
1883    // Overridden by ProfileAggregator.
1884    protected String computeLookupKeyForContact(SQLiteDatabase db, long contactId) {
1885        StringBuilder sb = new StringBuilder();
1886        mSelectionArgs1[0] = String.valueOf(contactId);
1887        final Cursor c = db.query(LookupKeyQuery.TABLE, LookupKeyQuery.COLUMNS,
1888                RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID);
1889        try {
1890            while (c.moveToNext()) {
1891                ContactLookupKey.appendToLookupKey(sb,
1892                        c.getString(LookupKeyQuery.ACCOUNT_TYPE_AND_DATA_SET),
1893                        c.getString(LookupKeyQuery.ACCOUNT_NAME),
1894                        c.getLong(LookupKeyQuery.ID),
1895                        c.getString(LookupKeyQuery.SOURCE_ID),
1896                        c.getString(LookupKeyQuery.DISPLAY_NAME));
1897            }
1898        } finally {
1899            c.close();
1900        }
1901        return sb.length() == 0 ? null : sb.toString();
1902    }
1903
1904    /**
1905     * Execute {@link SQLiteStatement} that will update the
1906     * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}.
1907     */
1908    public final void updateStarred(long rawContactId) {
1909        long contactId = mDbHelper.getContactId(rawContactId);
1910        if (contactId == 0) {
1911            return;
1912        }
1913
1914        mStarredUpdate.bindLong(1, contactId);
1915        mStarredUpdate.execute();
1916    }
1917
1918    /**
1919     * Execute {@link SQLiteStatement} that will update the
1920     * {@link Contacts#SEND_TO_VOICEMAIL} flag for the given {@link RawContacts#_ID}.
1921     */
1922    public final void updateSendToVoicemail(long rawContactId) {
1923        long contactId = mDbHelper.getContactId(rawContactId);
1924        if (contactId == 0) {
1925            return;
1926        }
1927
1928        mSendToVoicemailUpdate.bindLong(1, contactId);
1929        mSendToVoicemailUpdate.execute();
1930    }
1931
1932    /**
1933     * Execute {@link SQLiteStatement} that will update the
1934     * {@link Contacts#PINNED} flag for the given {@link RawContacts#_ID}.
1935     */
1936    public final void updatePinned(long rawContactId) {
1937        long contactId = mDbHelper.getContactId(rawContactId);
1938        if (contactId == 0) {
1939            return;
1940        }
1941        mPinnedUpdate.bindLong(1, contactId);
1942        mPinnedUpdate.execute();
1943    }
1944
1945    /**
1946     * Finds matching contacts and returns a cursor on those.
1947     */
1948    public final Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb,
1949            String[] projection, long contactId, int maxSuggestions, String filter,
1950            ArrayList<AggregationSuggestionParameter> parameters) {
1951        final SQLiteDatabase db = mDbHelper.getReadableDatabase();
1952        db.beginTransaction();
1953        try {
1954            List<MatchScore> bestMatches = findMatchingContacts(db, contactId, parameters);
1955            List<MatchScore> bestMatchesWithoutDuplicateContactIds = new ArrayList<>();
1956            Set<Long> contactIds = new HashSet<>();
1957            for (MatchScore bestMatch : bestMatches) {
1958                long cid = bestMatch.getContactId();
1959                if (!contactIds.contains(cid) && cid != contactId) {
1960                    bestMatchesWithoutDuplicateContactIds.add(bestMatch);
1961                    contactIds.add(cid);
1962                }
1963            }
1964            return queryMatchingContacts(qb, db, projection, bestMatchesWithoutDuplicateContactIds,
1965                    maxSuggestions, filter);
1966        } finally {
1967            db.endTransaction();
1968        }
1969    }
1970
1971    private interface ContactIdQuery {
1972        String[] COLUMNS = new String[] {
1973            Contacts._ID
1974        };
1975
1976        int _ID = 0;
1977    }
1978
1979    /**
1980     * Loads contacts with specified IDs and returns them in the order of IDs in the
1981     * supplied list.
1982     */
1983    private Cursor queryMatchingContacts(SQLiteQueryBuilder qb, SQLiteDatabase db,
1984            String[] projection, List<MatchScore> bestMatches, int maxSuggestions, String filter) {
1985        StringBuilder sb = new StringBuilder();
1986        sb.append(Contacts._ID);
1987        sb.append(" IN (");
1988        for (int i = 0; i < bestMatches.size(); i++) {
1989            MatchScore matchScore = bestMatches.get(i);
1990            if (i != 0) {
1991                sb.append(",");
1992            }
1993            sb.append(matchScore.getContactId());
1994        }
1995        sb.append(")");
1996
1997        if (!TextUtils.isEmpty(filter)) {
1998            sb.append(" AND " + Contacts._ID + " IN ");
1999            mContactsProvider.appendContactFilterAsNestedQuery(sb, filter);
2000        }
2001
2002        // Run a query and find ids of best matching contacts satisfying the filter (if any)
2003        HashSet<Long> foundIds = new HashSet<Long>();
2004        Cursor cursor = db.query(qb.getTables(), ContactIdQuery.COLUMNS, sb.toString(),
2005                null, null, null, null);
2006        try {
2007            while(cursor.moveToNext()) {
2008                foundIds.add(cursor.getLong(ContactIdQuery._ID));
2009            }
2010        } finally {
2011            cursor.close();
2012        }
2013
2014        // Exclude all contacts that did not match the filter
2015        Iterator<MatchScore> iter = bestMatches.iterator();
2016        while (iter.hasNext()) {
2017            long id = iter.next().getContactId();
2018            if (!foundIds.contains(id)) {
2019                iter.remove();
2020            }
2021        }
2022
2023        // Limit the number of returned suggestions
2024        final List<MatchScore> limitedMatches;
2025        if (bestMatches.size() > maxSuggestions) {
2026            limitedMatches = bestMatches.subList(0, maxSuggestions);
2027        } else {
2028            limitedMatches = bestMatches;
2029        }
2030
2031        // Build an in-clause with the remaining contact IDs
2032        sb.setLength(0);
2033        sb.append(Contacts._ID);
2034        sb.append(" IN (");
2035        for (int i = 0; i < limitedMatches.size(); i++) {
2036            MatchScore matchScore = limitedMatches.get(i);
2037            if (i != 0) {
2038                sb.append(",");
2039            }
2040            sb.append(matchScore.getContactId());
2041        }
2042        sb.append(")");
2043
2044        // Run the final query with the required projection and contact IDs found by the first query
2045        cursor = qb.query(db, projection, sb.toString(), null, null, null, Contacts._ID);
2046
2047        // Build a sorted list of discovered IDs
2048        ArrayList<Long> sortedContactIds = new ArrayList<Long>(limitedMatches.size());
2049        for (MatchScore matchScore : limitedMatches) {
2050            sortedContactIds.add(matchScore.getContactId());
2051        }
2052
2053        Collections.sort(sortedContactIds);
2054
2055        // Map cursor indexes according to the descending order of match scores
2056        int[] positionMap = new int[limitedMatches.size()];
2057        for (int i = 0; i < positionMap.length; i++) {
2058            long id = limitedMatches.get(i).getContactId();
2059            positionMap[i] = sortedContactIds.indexOf(id);
2060        }
2061
2062        return new ReorderingCursorWrapper(cursor, positionMap);
2063    }
2064
2065    /**
2066     * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
2067     * descending order of match score.
2068     * @param parameters
2069     */
2070    protected abstract List<MatchScore> findMatchingContacts(final SQLiteDatabase db,
2071            long contactId, ArrayList<AggregationSuggestionParameter> parameters);
2072
2073    public abstract void updateAggregationAfterVisibilityChange(long contactId);
2074}
2075