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