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