Home | History | Annotate | Download | only in aggregation
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.providers.contacts.aggregation;
     18 
     19 import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_PRIMARY;
     20 import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_SECONDARY;
     21 import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_SUGGEST;
     22 import android.database.Cursor;
     23 import android.database.sqlite.SQLiteDatabase;
     24 import android.provider.ContactsContract.AggregationExceptions;
     25 import android.provider.ContactsContract.CommonDataKinds.Email;
     26 import android.provider.ContactsContract.CommonDataKinds.Identity;
     27 import android.provider.ContactsContract.CommonDataKinds.Phone;
     28 import android.provider.ContactsContract.Contacts.AggregationSuggestions;
     29 import android.provider.ContactsContract.Data;
     30 import android.provider.ContactsContract.FullNameStyle;
     31 import android.provider.ContactsContract.PhotoFiles;
     32 import android.provider.ContactsContract.RawContacts;
     33 import android.text.TextUtils;
     34 import android.util.Log;
     35 import com.android.providers.contacts.ContactsDatabaseHelper;
     36 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
     37 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
     38 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
     39 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
     40 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
     41 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
     42 import com.android.providers.contacts.ContactsProvider2;
     43 import com.android.providers.contacts.NameSplitter;
     44 import com.android.providers.contacts.PhotoPriorityResolver;
     45 import com.android.providers.contacts.TransactionContext;
     46 import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
     47 import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper;
     48 import com.android.providers.contacts.aggregation.util.MatchScore;
     49 import com.android.providers.contacts.aggregation.util.RawContactMatcher;
     50 import com.android.providers.contacts.aggregation.util.RawContactMatchingCandidates;
     51 import com.android.providers.contacts.database.ContactsTableUtil;
     52 import com.google.android.collect.Sets;
     53 import com.google.common.collect.HashMultimap;
     54 import com.google.common.collect.Multimap;
     55 
     56 import java.util.ArrayList;
     57 import java.util.HashSet;
     58 import java.util.List;
     59 import java.util.Map;
     60 import java.util.Set;
     61 
     62 /**
     63  * ContactAggregator2 deals with aggregating contact information with sufficient matching data
     64  * points. E.g., two John Doe contacts with same phone numbers are presumed to be the same
     65  * person unless the user declares otherwise.
     66  */
     67 public class ContactAggregator2 extends AbstractContactAggregator {
     68 
     69     // Possible operation types for contacts aggregation.
     70     private static final int CREATE_NEW_CONTACT = 1;
     71     private static final int KEEP_INTACT = 0;
     72     private static final int RE_AGGREGATE = -1;
     73 
     74     private final RawContactMatcher mMatcher = new RawContactMatcher();
     75 
     76     /**
     77      * Constructor.
     78      */
     79     public ContactAggregator2(ContactsProvider2 contactsProvider,
     80             ContactsDatabaseHelper contactsDatabaseHelper,
     81             PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter,
     82             CommonNicknameCache commonNicknameCache) {
     83         super(contactsProvider, contactsDatabaseHelper, photoPriorityResolver, nameSplitter,
     84                 commonNicknameCache);
     85     }
     86 
     87     /**
     88      * Given a specific raw contact, finds all matching raw contacts and re-aggregate them
     89      * based on the matching connectivity.
     90      */
     91      synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db,
     92              long rawContactId, long accountId, long currentContactId,
     93              MatchCandidateList candidates) {
     94 
     95          if (!needAggregate(db, rawContactId)) {
     96              if (VERBOSE_LOGGING) {
     97                  Log.v(TAG, "Skip rid=" + rawContactId + " which has already been aggregated.");
     98              }
     99              return;
    100          }
    101 
    102          if (VERBOSE_LOGGING) {
    103             Log.v(TAG, "aggregateContact: rid=" + rawContactId + " cid=" + currentContactId);
    104         }
    105 
    106         int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
    107 
    108         Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId);
    109         if (aggModeObject != null) {
    110             aggregationMode = aggModeObject;
    111         }
    112 
    113         RawContactMatcher matcher = new RawContactMatcher();
    114         RawContactMatchingCandidates matchingCandidates = new RawContactMatchingCandidates();
    115         if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
    116             // If this is a newly inserted contact or a visible contact, look for
    117             // data matches.
    118             if (currentContactId == 0
    119                     || mDbHelper.isContactInDefaultDirectory(db, currentContactId)) {
    120                 // Find the set of matching candidates
    121                 matchingCandidates = findRawContactMatchingCandidates(db, rawContactId, candidates,
    122                         matcher);
    123             }
    124         } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
    125             return;
    126         }
    127 
    128         // # of raw_contacts in the [currentContactId] contact excluding the [rawContactId]
    129         // raw_contact.
    130         long currentContactContentsCount = 0;
    131 
    132         if (currentContactId != 0) {
    133             mRawContactCountQuery.bindLong(1, currentContactId);
    134             mRawContactCountQuery.bindLong(2, rawContactId);
    135             currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong();
    136         }
    137 
    138         // Set aggregation operation, i.e., re-aggregate, keep intact, or create new contact based
    139         // on the number of matching candidates and the number of raw_contacts in the
    140         // [currentContactId] excluding the [rawContactId].
    141         final int operation;
    142         final int candidatesCount = matchingCandidates.getCount();
    143         if (candidatesCount >= AGGREGATION_CONTACT_SIZE_LIMIT) {
    144             operation = KEEP_INTACT;
    145             if (VERBOSE_LOGGING) {
    146                 Log.v(TAG, "Too many matching raw contacts (" + candidatesCount
    147                         + ") are found, so skip aggregation");
    148             }
    149         } else if (candidatesCount > 0) {
    150             operation = RE_AGGREGATE;
    151         } else {
    152             // When there is no matching raw contact found, if there are no other raw contacts in
    153             // the current aggregate, we might as well reuse it. Also, if the aggregation mode is
    154             // SUSPENDED, we must reuse the same aggregate.
    155             if (currentContactId != 0
    156                     && (currentContactContentsCount == 0
    157                     || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) {
    158                 operation = KEEP_INTACT;
    159             } else {
    160                 operation = CREATE_NEW_CONTACT;
    161             }
    162         }
    163 
    164         if (operation == KEEP_INTACT) {
    165             // Aggregation unchanged
    166             if (VERBOSE_LOGGING) {
    167                 Log.v(TAG, "Aggregation unchanged");
    168             }
    169             markAggregated(db, String.valueOf(rawContactId));
    170         } else if (operation == CREATE_NEW_CONTACT) {
    171             // create new contact for [rawContactId]
    172             if (VERBOSE_LOGGING) {
    173                 Log.v(TAG, "create new contact for rid=" + rawContactId);
    174             }
    175             createContactForRawContacts(db, txContext, Sets.newHashSet(rawContactId), null);
    176             if (currentContactContentsCount > 0) {
    177                 updateAggregateData(txContext, currentContactId);
    178             }
    179             markAggregated(db, String.valueOf(rawContactId));
    180         } else {
    181             // re-aggregate
    182             if (VERBOSE_LOGGING) {
    183                 Log.v(TAG, "Re-aggregating rids=" + rawContactId + ","
    184                         + TextUtils.join(",", matchingCandidates.getRawContactIdSet()));
    185             }
    186             reAggregateRawContacts(txContext, db, currentContactId, rawContactId, accountId,
    187                     currentContactContentsCount, matchingCandidates);
    188         }
    189     }
    190 
    191     private boolean needAggregate(SQLiteDatabase db, long rawContactId) {
    192         final String sql = "SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS +
    193                 " WHERE " + RawContactsColumns.AGGREGATION_NEEDED + "=1" +
    194                 " AND " + RawContacts._ID + "=?";
    195 
    196         mSelectionArgs1[0] = String.valueOf(rawContactId);
    197         final Cursor cursor = db.rawQuery(sql, mSelectionArgs1);
    198 
    199         try {
    200             return cursor.getCount() != 0;
    201         } finally {
    202             cursor.close();
    203         }
    204     }
    205     /**
    206      * Find the set of matching raw contacts for given rawContactId. Add all the raw contact
    207      * candidates with matching scores > threshold to RawContactMatchingCandidates. Keep doing
    208      * this for every raw contact in RawContactMatchingCandidates until is it not changing.
    209      */
    210     private RawContactMatchingCandidates findRawContactMatchingCandidates(SQLiteDatabase db, long
    211             rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) {
    212         updateMatchScores(db, rawContactId, candidates, matcher);
    213         final RawContactMatchingCandidates matchingCandidates = new RawContactMatchingCandidates(
    214                 matcher.pickBestMatches());
    215         Set<Long> newIds = new HashSet<>();
    216         newIds.addAll(matchingCandidates.getRawContactIdSet());
    217         // Keep doing the following until no new raw contact candidate is found.
    218         while (!newIds.isEmpty()) {
    219             if (matchingCandidates.getCount() >= AGGREGATION_CONTACT_SIZE_LIMIT) {
    220                 return matchingCandidates;
    221             }
    222             final Set<Long> tmpIdSet = new HashSet<>();
    223             for (long rId : newIds) {
    224                 final RawContactMatcher rMatcher = new RawContactMatcher();
    225                 updateMatchScores(db, rId, new MatchCandidateList(),
    226                         rMatcher);
    227                 List<MatchScore> newMatches = rMatcher.pickBestMatches();
    228                 for (MatchScore newMatch : newMatches) {
    229                     final long newRawContactId = newMatch.getRawContactId();
    230                     if (!matchingCandidates.getRawContactIdSet().contains(newRawContactId)) {
    231                         tmpIdSet.add(newRawContactId);
    232                         matchingCandidates.add(newMatch);
    233                     }
    234                 }
    235             }
    236             newIds.clear();
    237             newIds.addAll(tmpIdSet);
    238         }
    239         return matchingCandidates;
    240     }
    241 
    242     /**
    243      * Find out which mime-types are shared by more than one contacts for {@code rawContactIds}.
    244      * Clear the is_super_primary settings for these mime-types.
    245      * {@code rawContactIds} should be a comma separated ID list.
    246      */
    247      private void clearSuperPrimarySetting(SQLiteDatabase db, String rawContactIds) {
    248         final String sql =
    249                 "SELECT " + DataColumns.MIMETYPE_ID + ", count(1) c  FROM " +
    250                         Tables.DATA +" WHERE " + Data.IS_SUPER_PRIMARY + " = 1 AND " +
    251                         Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ") group by " +
    252                         DataColumns.MIMETYPE_ID + " HAVING c > 1";
    253 
    254         // Find out which mime-types exist with is_super_primary=true on more then one contacts.
    255         int index = 0;
    256         final StringBuilder mimeTypeCondition = new StringBuilder();
    257         mimeTypeCondition.append(" AND " + DataColumns.MIMETYPE_ID + " IN (");
    258 
    259         final Cursor c = db.rawQuery(sql, null);
    260         try {
    261             c.moveToPosition(-1);
    262             while (c.moveToNext()) {
    263                 if (index > 0) {
    264                     mimeTypeCondition.append(',');
    265                 }
    266                 mimeTypeCondition.append(c.getLong((0)));
    267                 index++;
    268             }
    269         } finally {
    270             c.close();
    271         }
    272 
    273         if (index == 0) {
    274             return;
    275         }
    276 
    277         // Clear is_super_primary setting for all the mime-types with is_super_primary=true
    278         // in both raw contact of rawContactId and raw contacts of contactId
    279         String superPrimaryUpdateSql = "UPDATE " + Tables.DATA +
    280                 " SET " + Data.IS_SUPER_PRIMARY + "=0" +
    281                 " WHERE " +  Data.RAW_CONTACT_ID +
    282                 " IN (" + rawContactIds + ")";
    283 
    284         mimeTypeCondition.append(')');
    285         superPrimaryUpdateSql += mimeTypeCondition.toString();
    286         db.execSQL(superPrimaryUpdateSql);
    287     }
    288 
    289     private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
    290             int aggregationType, boolean countOnly) {
    291         final String idPairSelection =  "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " +
    292                 AggregationExceptions.RAW_CONTACT_ID2;
    293         final String sql =
    294                 " FROM " + Tables.AGGREGATION_EXCEPTIONS +
    295                 " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" +
    296                         rawContactIdSet1 + ")" +
    297                 " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" +
    298                 " AND " + AggregationExceptions.TYPE + "=" + aggregationType;
    299         return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
    300                 idPairSelection + sql;
    301     }
    302 
    303     /**
    304      * Re-aggregate rawContact of {@code rawContactId} and all the raw contacts of
    305      * {@code matchingCandidates} into connected components. This only happens when a given
    306      * raw contacts cannot be joined with its best matching contacts directly.
    307      *
    308      *  Two raw contacts are considered connected if they share at least one email address, phone
    309      *  number or identity. Create new contact for each connected component except the very first
    310      *  one that doesn't contain rawContactId of {@code rawContactId}.
    311      */
    312     private void reAggregateRawContacts(TransactionContext txContext, SQLiteDatabase db,
    313             long currentCidForRawContact, long rawContactId, long accountId,
    314             long currentContactContentsCount, RawContactMatchingCandidates matchingCandidates) {
    315         // Find the connected component based on the aggregation exceptions or
    316         // identity/email/phone matching for all the raw contacts of [contactId] and the give
    317         // raw contact.
    318         final Set<Long> allIds = new HashSet<>();
    319         allIds.add(rawContactId);
    320         allIds.addAll(matchingCandidates.getRawContactIdSet());
    321         final Set<Set<Long>> connectedRawContactSets = findConnectedRawContacts(db, allIds);
    322 
    323         final Map<Long, Long> rawContactsToAccounts = matchingCandidates.getRawContactToAccount();
    324         rawContactsToAccounts.put(rawContactId, accountId);
    325         ContactAggregatorHelper.mergeComponentsWithDisjointAccounts(connectedRawContactSets,
    326                 rawContactsToAccounts);
    327         breakComponentsByExceptions(db, connectedRawContactSets);
    328 
    329         // Create new contact for each connected component. Use the first reusable contactId if
    330         // possible. If no reusable contactId found, create new contact for the connected component.
    331         // Update aggregate data for all the contactIds touched by this connected component,
    332         for (Set<Long> connectedRawContactIds : connectedRawContactSets) {
    333             Long contactId = null;
    334             Set<Long> cidsNeedToBeUpdated = new HashSet<>();
    335             if (connectedRawContactIds.contains(rawContactId)) {
    336                 // If there is no other raw contacts aggregated with the given raw contact currently
    337                 // or all the raw contacts in [currentCidForRawContact] are still in the same
    338                 // connected component, we might as well reuse it.
    339                 if (currentCidForRawContact != 0 &&
    340                         (currentContactContentsCount == 0) ||
    341                         canBeReused(db, currentCidForRawContact, connectedRawContactIds)) {
    342                     contactId = currentCidForRawContact;
    343                     for (Long connectedRawContactId : connectedRawContactIds) {
    344                         Long cid = matchingCandidates.getContactId(connectedRawContactId);
    345                         if (cid != null && !cid.equals(contactId)) {
    346                             cidsNeedToBeUpdated.add(cid);
    347                         }
    348                     }
    349                 } else if (currentCidForRawContact != 0){
    350                     cidsNeedToBeUpdated.add(currentCidForRawContact);
    351                 }
    352             } else {
    353                 boolean foundContactId = false;
    354                 for (Long connectedRawContactId : connectedRawContactIds) {
    355                     Long currentContactId = matchingCandidates.getContactId(connectedRawContactId);
    356                     if (!foundContactId && currentContactId != null &&
    357                             canBeReused(db, currentContactId, connectedRawContactIds)) {
    358                         contactId = currentContactId;
    359                         foundContactId = true;
    360                     } else {
    361                         cidsNeedToBeUpdated.add(currentContactId);
    362                     }
    363                 }
    364             }
    365             final String connectedRids = TextUtils.join(",", connectedRawContactIds);
    366             clearSuperPrimarySetting(db, connectedRids);
    367             createContactForRawContacts(db, txContext, connectedRawContactIds, contactId);
    368             // re-aggregate
    369             if (VERBOSE_LOGGING) {
    370                 Log.v(TAG, "Aggregating rids=" + connectedRawContactIds);
    371             }
    372             markAggregated(db, connectedRids);
    373 
    374             for (Long cid : cidsNeedToBeUpdated) {
    375                 long currentRcCount = 0;
    376                 if (cid != 0) {
    377                     mRawContactCountQuery.bindLong(1, cid);
    378                     mRawContactCountQuery.bindLong(2, 0);
    379                     currentRcCount = mRawContactCountQuery.simpleQueryForLong();
    380                 }
    381 
    382                 if (currentRcCount == 0) {
    383                     // Delete a contact if it doesn't contain anything
    384                     ContactsTableUtil.deleteContact(db, cid);
    385                     mAggregatedPresenceDelete.bindLong(1, cid);
    386                     mAggregatedPresenceDelete.execute();
    387                 } else {
    388                     updateAggregateData(txContext, cid);
    389                 }
    390             }
    391         }
    392     }
    393 
    394     /**
    395      * Check if contactId can be reused as the contact Id for new aggregation of all the
    396      * connectedRawContactIds. If connectedRawContactIds set contains all the raw contacts
    397      * currently aggregated under contactId, return true; Otherwise, return false.
    398      */
    399     private boolean canBeReused(SQLiteDatabase db, Long contactId,
    400             Set<Long> connectedRawContactIds) {
    401         final String sql = "SELECT " + RawContactsColumns.CONCRETE_ID + " FROM " +
    402                 Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=? AND " +
    403                 RawContacts.DELETED + "=0";
    404         mSelectionArgs1[0] = String.valueOf(contactId);
    405         final Cursor cursor = db.rawQuery(sql, mSelectionArgs1);
    406         try {
    407             cursor.moveToPosition(-1);
    408             while (cursor.moveToNext()) {
    409                 if (!connectedRawContactIds.contains(cursor.getLong(0))) {
    410                     return false;
    411                 }
    412             }
    413         } finally {
    414             cursor.close();
    415         }
    416         return true;
    417     }
    418 
    419     /**
    420      * Separate all the raw_contacts which has "SEPARATE" aggregation exception to another
    421      * raw_contacts in the same component.
    422      */
    423     private void breakComponentsByExceptions(SQLiteDatabase db,
    424             Set<Set<Long>> connectedRawContacts) {
    425         final Set<Set<Long>> tmpSets = new HashSet<>(connectedRawContacts);
    426         for (Set<Long> component : tmpSets) {
    427             final String rawContacts = TextUtils.join(",", component);
    428             // If "SEPARATE" exception is found inside an connected component [component],
    429             // remove the [component] from [connectedRawContacts], and create new connected
    430             // components for all raw contacts of [component] solely based on "JOIN" exceptions
    431             // and add them to [connectedRawContacts].
    432             if (isFirstColumnGreaterThanZero(db, buildExceptionMatchingSql(rawContacts, rawContacts,
    433                     AggregationExceptions.TYPE_KEEP_SEPARATE, /* countOnly =*/true))) {
    434                 Multimap<Long, Long> joinPairs = HashMultimap.create();
    435                 findIdPairs(db, buildExceptionMatchingSql(rawContacts, rawContacts), joinPairs);
    436                 connectedRawContacts.remove(component);
    437                 connectedRawContacts.addAll(
    438                     ContactAggregatorHelper.findConnectedComponents(component, joinPairs));
    439             }
    440         }
    441     }
    442 
    443     /**
    444      * Ensures that automatic aggregation rules are followed after a contact
    445      * becomes visible or invisible. Specifically, consider this case: there are
    446      * three contacts named Foo. Two of them come from account A1 and one comes
    447      * from account A2. The aggregation rules say that in this case none of the
    448      * three Foo's should be aggregated: two of them are in the same account, so
    449      * they don't get aggregated; the third has two affinities, so it does not
    450      * join either of them.
    451      * <p>
    452      * Consider what happens if one of the "Foo"s from account A1 becomes
    453      * invisible. Nothing stands in the way of aggregating the other two
    454      * anymore, so they should get joined.
    455      * <p>
    456      * What if the invisible "Foo" becomes visible after that? We should split the
    457      * aggregate between the other two.
    458      */
    459     public void updateAggregationAfterVisibilityChange(long contactId) {
    460         SQLiteDatabase db = mDbHelper.getWritableDatabase();
    461         boolean visible = mDbHelper.isContactInDefaultDirectory(db, contactId);
    462         if (visible) {
    463             markContactForAggregation(db, contactId);
    464         } else {
    465             // Find all contacts that _could be_ aggregated with this one and
    466             // rerun aggregation for all of them
    467             mSelectionArgs1[0] = String.valueOf(contactId);
    468             Cursor cursor = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
    469                     RawContactIdQuery.SELECTION, mSelectionArgs1, null, null, null);
    470             try {
    471                 while (cursor.moveToNext()) {
    472                     long rawContactId = cursor.getLong(RawContactIdQuery.RAW_CONTACT_ID);
    473                     mMatcher.clear();
    474 
    475                     updateMatchScoresBasedOnIdentityMatch(db, rawContactId, mMatcher);
    476                     updateMatchScoresBasedOnNameMatches(db, rawContactId, mMatcher);
    477                     List<MatchScore> bestMatches =
    478                             mMatcher.pickBestMatches(SCORE_THRESHOLD_PRIMARY);
    479                     for (MatchScore matchScore : bestMatches) {
    480                         markContactForAggregation(db, matchScore.getContactId());
    481                     }
    482 
    483                     mMatcher.clear();
    484                     updateMatchScoresBasedOnEmailMatches(db, rawContactId, mMatcher);
    485                     updateMatchScoresBasedOnPhoneMatches(db, rawContactId, mMatcher);
    486                     bestMatches =
    487                             mMatcher.pickBestMatches(SCORE_THRESHOLD_SECONDARY);
    488                     for (MatchScore matchScore : bestMatches) {
    489                         markContactForAggregation(db, matchScore.getContactId());
    490                     }
    491                 }
    492             } finally {
    493                 cursor.close();
    494             }
    495         }
    496     }
    497 
    498     /**
    499      * Computes match scores based on exceptions entered by the user: always match and never match.
    500      */
    501     private void updateMatchScoresBasedOnExceptions(SQLiteDatabase db, long rawContactId,
    502             RawContactMatcher matcher) {
    503         if (!mAggregationExceptionIdsValid) {
    504             prefetchAggregationExceptionIds(db);
    505         }
    506 
    507         // If there are no aggregation exceptions involving this raw contact, there is no need to
    508         // run a query and we can just return -1, which stands for "nothing found"
    509         if (!mAggregationExceptionIds.contains(rawContactId)) {
    510             return;
    511         }
    512 
    513         final Cursor c = db.query(AggregateExceptionQuery.TABLE,
    514                 AggregateExceptionQuery.COLUMNS,
    515                 AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId
    516                         + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId,
    517                 null, null, null, null);
    518 
    519         try {
    520             while (c.moveToNext()) {
    521                 int type = c.getInt(AggregateExceptionQuery.TYPE);
    522                 long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1);
    523                 long contactId = -1;
    524                 long rId = -1;
    525                 long accountId = -1;
    526                 if (rawContactId == rawContactId1) {
    527                     if (!c.isNull(AggregateExceptionQuery.RAW_CONTACT_ID2)) {
    528                         rId = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID2);
    529                         contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2);
    530                         accountId = c.getLong(AggregateExceptionQuery.ACCOUNT_ID2);
    531                     }
    532                 } else {
    533                     if (!c.isNull(AggregateExceptionQuery.RAW_CONTACT_ID1)) {
    534                         rId = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1);
    535                         contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1);
    536                         accountId = c.getLong(AggregateExceptionQuery.ACCOUNT_ID1);
    537                     }
    538                 }
    539                 if (rId != -1) {
    540                     if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) {
    541                         matcher.keepIn(rId, contactId, accountId);
    542                     } else {
    543                         matcher.keepOut(rId, contactId, accountId);
    544                     }
    545                 }
    546             }
    547         } finally {
    548             c.close();
    549         }
    550     }
    551 
    552     /**
    553      * Finds contacts with exact identity matches to the the specified raw contact.
    554      */
    555     private void updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId,
    556             RawContactMatcher matcher) {
    557         mSelectionArgs2[0] = String.valueOf(rawContactId);
    558         mSelectionArgs2[1] = String.valueOf(mMimeTypeIdIdentity);
    559         Cursor c = db.query(IdentityLookupMatchQuery.TABLE, IdentityLookupMatchQuery.COLUMNS,
    560                 IdentityLookupMatchQuery.SELECTION,
    561                 mSelectionArgs2, RawContacts.CONTACT_ID, null, null);
    562         try {
    563             while (c.moveToNext()) {
    564                 final long rId = c.getLong(IdentityLookupMatchQuery.RAW_CONTACT_ID);
    565                 if (rId == rawContactId) {
    566                     continue;
    567                 }
    568                 final long contactId = c.getLong(IdentityLookupMatchQuery.CONTACT_ID);
    569                 final long accountId = c.getLong(IdentityLookupMatchQuery.ACCOUNT_ID);
    570                 matcher.matchIdentity(rId, contactId, accountId);
    571             }
    572         } finally {
    573             c.close();
    574         }
    575     }
    576 
    577     /**
    578      * Finds contacts with names matching the name of the specified raw contact.
    579      */
    580     private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
    581             RawContactMatcher matcher) {
    582         mSelectionArgs1[0] = String.valueOf(rawContactId);
    583         Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS,
    584                 NameLookupMatchQuery.SELECTION,
    585                 mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING);
    586         try {
    587             while (c.moveToNext()) {
    588                 long rId =  c.getLong(NameLookupMatchQuery.RAW_CONTACT_ID);
    589                 if (rId == rawContactId) {
    590                     continue;
    591                 }
    592                 long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID);
    593                 long accountId = c.getLong(NameLookupMatchQuery.ACCOUNT_ID);
    594                 String name = c.getString(NameLookupMatchQuery.NAME);
    595                 int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A);
    596                 int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B);
    597                 matcher.matchName(rId, contactId, accountId, nameTypeA, name,
    598                         nameTypeB, name, RawContactMatcher.MATCHING_ALGORITHM_EXACT);
    599                 if (nameTypeA == NameLookupType.NICKNAME &&
    600                         nameTypeB == NameLookupType.NICKNAME) {
    601                     matcher.updateScoreWithNicknameMatch(rId, contactId, accountId);
    602                 }
    603             }
    604         } finally {
    605             c.close();
    606         }
    607     }
    608 
    609     private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
    610             RawContactMatcher matcher) {
    611         mSelectionArgs2[0] = String.valueOf(rawContactId);
    612         mSelectionArgs2[1] = String.valueOf(mMimeTypeIdEmail);
    613         Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
    614                 EmailLookupQuery.SELECTION,
    615                 mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
    616         try {
    617             while (c.moveToNext()) {
    618                 long rId = c.getLong(EmailLookupQuery.RAW_CONTACT_ID);
    619                 if (rId == rawContactId) {
    620                     continue;
    621                 }
    622                 long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
    623                 long accountId = c.getLong(EmailLookupQuery.ACCOUNT_ID);
    624                 matcher.updateScoreWithEmailMatch(rId, contactId, accountId);
    625             }
    626         } finally {
    627             c.close();
    628         }
    629     }
    630 
    631     /**
    632      * Finds contacts with names matching the specified name.
    633      */
    634     private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query,
    635             MatchCandidateList candidates, RawContactMatcher matcher) {
    636         candidates.clear();
    637         NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder(
    638                 mNameSplitter, candidates);
    639         builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED);
    640         if (builder.isEmpty()) {
    641             return;
    642         }
    643 
    644         Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE,
    645                 NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null,
    646                 null, PRIMARY_HIT_LIMIT_STRING);
    647         try {
    648             while (c.moveToNext()) {
    649                 long rId = c.getLong(NameLookupMatchQueryWithParameter.RAW_CONTACT_ID);
    650                 long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID);
    651                 long accountId = c.getLong(NameLookupMatchQueryWithParameter.ACCOUNT_ID);
    652                 String name = c.getString(NameLookupMatchQueryWithParameter.NAME);
    653                 int nameTypeA = builder.getLookupType(name);
    654                 int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE);
    655                 matcher.matchName(rId, contactId, accountId, nameTypeA, name, nameTypeB, name,
    656                         RawContactMatcher.MATCHING_ALGORITHM_EXACT);
    657                 if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) {
    658                     matcher.updateScoreWithNicknameMatch(rId, contactId, accountId);
    659                 }
    660             }
    661         } finally {
    662             c.close();
    663         }
    664     }
    665 
    666     private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
    667             RawContactMatcher matcher) {
    668         mSelectionArgs2[0] = String.valueOf(rawContactId);
    669         mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter();
    670         Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
    671                 PhoneLookupQuery.SELECTION,
    672                 mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
    673         try {
    674             while (c.moveToNext()) {
    675                 long rId = c.getLong(PhoneLookupQuery.RAW_CONTACT_ID);
    676                 if (rId == rawContactId) {
    677                     continue;
    678                 }
    679                 long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
    680                 long accountId = c.getLong(PhoneLookupQuery.ACCOUNT_ID);
    681                 matcher.updateScoreWithPhoneNumberMatch(rId, contactId, accountId);
    682             }
    683         } finally {
    684             c.close();
    685         }
    686     }
    687 
    688     /**
    689      * Loads name lookup rows for approximate name matching and updates match scores based on that
    690      * data.
    691      */
    692     private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
    693             RawContactMatcher matcher) {
    694         HashSet<String> firstLetters = new HashSet<>();
    695         for (int i = 0; i < candidates.mCount; i++) {
    696             final NameMatchCandidate candidate = candidates.mList.get(i);
    697             if (candidate.mName.length() >= 2) {
    698                 String firstLetter = candidate.mName.substring(0, 2);
    699                 if (!firstLetters.contains(firstLetter)) {
    700                     firstLetters.add(firstLetter);
    701                     final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '"
    702                             + firstLetter + "*') AND "
    703                             + "(" + NameLookupColumns.NAME_TYPE + " IN("
    704                                     + NameLookupType.NAME_COLLATION_KEY + ","
    705                                     + NameLookupType.EMAIL_BASED_NICKNAME + ","
    706                                     + NameLookupType.NICKNAME + ")) AND "
    707                             + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
    708                     matchAllCandidates(db, selection, candidates, matcher,
    709                             RawContactMatcher.MATCHING_ALGORITHM_APPROXIMATE,
    710                             String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT));
    711                 }
    712             }
    713         }
    714     }
    715 
    716     private interface ContactNameLookupQuery {
    717         String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
    718 
    719         String[] COLUMNS = new String[] {
    720                 RawContacts._ID,
    721                 RawContacts.CONTACT_ID,
    722                 RawContactsColumns.ACCOUNT_ID,
    723                 NameLookupColumns.NORMALIZED_NAME,
    724                 NameLookupColumns.NAME_TYPE
    725         };
    726 
    727         int RAW_CONTACT_ID = 0;
    728         int CONTACT_ID = 1;
    729         int ACCOUNT_ID = 2;
    730         int NORMALIZED_NAME = 3;
    731         int NAME_TYPE = 4;
    732     }
    733 
    734     /**
    735      * Loads all candidate rows from the name lookup table and updates match scores based
    736      * on that data.
    737      */
    738     private void matchAllCandidates(SQLiteDatabase db, String selection,
    739             MatchCandidateList candidates, RawContactMatcher matcher, int algorithm, String limit) {
    740         final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
    741                 selection, null, null, null, null, limit);
    742 
    743         try {
    744             while (c.moveToNext()) {
    745                 Long rawContactId = c.getLong(ContactNameLookupQuery.RAW_CONTACT_ID);
    746                 Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
    747                 Long accountId = c.getLong(ContactNameLookupQuery.ACCOUNT_ID);
    748                 String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
    749                 int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
    750 
    751                 // Note the N^2 complexity of the following fragment. This is not a huge concern
    752                 // since the number of candidates is very small and in general secondary hits
    753                 // in the absence of primary hits are rare.
    754                 for (int i = 0; i < candidates.mCount; i++) {
    755                     NameMatchCandidate candidate = candidates.mList.get(i);
    756                     matcher.matchName(rawContactId, contactId, accountId, candidate.mLookupType,
    757                             candidate.mName, nameType, name, algorithm);
    758                 }
    759             }
    760         } finally {
    761             c.close();
    762         }
    763     }
    764 
    765     private interface PhotoFileQuery {
    766         final String[] COLUMNS = new String[] {
    767                 PhotoFiles.HEIGHT,
    768                 PhotoFiles.WIDTH,
    769                 PhotoFiles.FILESIZE
    770         };
    771 
    772         int HEIGHT = 0;
    773         int WIDTH = 1;
    774         int FILESIZE = 2;
    775     }
    776 
    777     private class PhotoEntry implements Comparable<PhotoEntry> {
    778         // Pixel count (width * height) for the image.
    779         final int pixelCount;
    780 
    781         // File size (in bytes) of the image.  Not populated if the image is a thumbnail.
    782         final int fileSize;
    783 
    784         private PhotoEntry(int pixelCount, int fileSize) {
    785             this.pixelCount = pixelCount;
    786             this.fileSize = fileSize;
    787         }
    788 
    789         @Override
    790         public int compareTo(PhotoEntry pe) {
    791             if (pe == null) {
    792                 return -1;
    793             }
    794             if (pixelCount == pe.pixelCount) {
    795                 return pe.fileSize - fileSize;
    796             } else {
    797                 return pe.pixelCount - pixelCount;
    798             }
    799         }
    800     }
    801 
    802     /**
    803      * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
    804      * descending order of match score.
    805      * @param parameters
    806      */
    807     protected List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId,
    808             ArrayList<AggregationSuggestionParameter> parameters) {
    809 
    810         MatchCandidateList candidates = new MatchCandidateList();
    811         RawContactMatcher matcher = new RawContactMatcher();
    812 
    813         if (parameters == null || parameters.size() == 0) {
    814             final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
    815                     RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
    816             try {
    817                 while (c.moveToNext()) {
    818                     long rawContactId = c.getLong(RawContactIdQuery.RAW_CONTACT_ID);
    819                     long accountId = c.getLong(RawContactIdQuery.ACCOUNT_ID);
    820                     // Don't aggregate a contact with its own raw contacts.
    821                     matcher.keepOut(rawContactId, contactId, accountId);
    822                     updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
    823                             matcher);
    824                 }
    825             } finally {
    826                 c.close();
    827             }
    828         } else {
    829             updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates,
    830                     matcher, parameters);
    831         }
    832 
    833         return matcher.pickBestMatches(SCORE_THRESHOLD_SUGGEST);
    834     }
    835 
    836     /**
    837      * Computes suggestion scores for contacts that have matching data rows.
    838      * Aggregation suggestion doesn't consider aggregation exceptions, but is purely based on the
    839      * raw contacts information.
    840      */
    841     private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
    842             long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) {
    843 
    844         updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
    845         updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
    846         updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
    847         updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
    848         loadNameMatchCandidates(db, rawContactId, candidates, false);
    849         lookupApproximateNameMatches(db, candidates, matcher);
    850     }
    851 
    852     /**
    853      * Computes scores for contacts that have matching data rows.
    854      */
    855     private void updateMatchScores(SQLiteDatabase db, long rawContactId,
    856             MatchCandidateList candidates, RawContactMatcher matcher) {
    857         //update primary score
    858         updateMatchScoresBasedOnExceptions(db, rawContactId, matcher);
    859         updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
    860         // update scores only if the raw contact doesn't have structured name
    861         if (rawContactWithoutName(db, rawContactId)) {
    862             updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
    863             updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
    864             updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
    865             final List<Long> secondaryRawContactIds = matcher.prepareSecondaryMatchCandidates();
    866             if (secondaryRawContactIds != null
    867                     && secondaryRawContactIds.size() <= SECONDARY_HIT_LIMIT) {
    868                 updateScoreForCandidatesWithoutName(db, secondaryRawContactIds, matcher);
    869             }
    870         }
    871     }
    872 
    873     private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
    874             MatchCandidateList candidates, RawContactMatcher matcher,
    875             ArrayList<AggregationSuggestionParameter> parameters) {
    876         for (AggregationSuggestionParameter parameter : parameters) {
    877             if (AggregationSuggestions.PARAMETER_MATCH_NAME.equals(parameter.kind)) {
    878                 updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher);
    879             }
    880 
    881             // TODO: add support for other parameter kinds
    882         }
    883     }
    884 
    885     private boolean rawContactWithoutName(SQLiteDatabase db, long rawContactId) {
    886         String selection = RawContacts._ID + " =" + rawContactId;
    887         final Cursor c = db.query(NullNameRawContactsIdsQuery.TABLE,
    888                 NullNameRawContactsIdsQuery.COLUMNS, selection, null, null, null, null);
    889 
    890         try {
    891             if (c.moveToFirst()) {
    892                 return TextUtils.isEmpty(c.getString(NullNameRawContactsIdsQuery.NAME));
    893             }
    894         } finally {
    895             c.close();
    896         }
    897         return false;
    898     }
    899 
    900     /**
    901      * Update scores for matches with secondary data matching but no structured name.
    902      */
    903     private void updateScoreForCandidatesWithoutName(SQLiteDatabase db,
    904             List<Long> secondaryRawContactIds, RawContactMatcher matcher) {
    905 
    906         mSb.setLength(0);
    907 
    908         mSb.append(RawContacts._ID).append(" IN (");
    909         for (int i = 0; i < secondaryRawContactIds.size(); i++) {
    910             if (i != 0) {
    911                 mSb.append(",");
    912             }
    913             mSb.append(secondaryRawContactIds.get(i));
    914         }
    915         mSb.append( ")");
    916         final Cursor c = db.query(NullNameRawContactsIdsQuery.TABLE,
    917                 NullNameRawContactsIdsQuery.COLUMNS, mSb.toString(), null, null, null, null);
    918 
    919         try {
    920             while (c.moveToNext()) {
    921                 Long rId = c.getLong(NullNameRawContactsIdsQuery.RAW_CONTACT_ID);
    922                 Long contactId = c.getLong(NullNameRawContactsIdsQuery.CONTACT_ID);
    923                 Long accountId = c.getLong(NullNameRawContactsIdsQuery.ACCOUNT_ID);
    924                 String name = c.getString(NullNameRawContactsIdsQuery.NAME);
    925                 if (TextUtils.isEmpty(name)) {
    926                     matcher.matchNoName(rId, contactId, accountId);
    927                 }
    928             }
    929         } finally {
    930             c.close();
    931         }
    932     }
    933 
    934     protected interface IdentityLookupMatchQuery {
    935         final String TABLE = Tables.DATA + " dataA"
    936                 + " JOIN " + Tables.DATA + " dataB" +
    937                 " ON (dataA." + Identity.NAMESPACE + "=dataB." + Identity.NAMESPACE +
    938                 " AND dataA." + Identity.IDENTITY + "=dataB." + Identity.IDENTITY + ")"
    939                 + " JOIN " + Tables.RAW_CONTACTS +
    940                 " ON (dataB." + Data.RAW_CONTACT_ID + " = "
    941                 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
    942 
    943         final String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
    944                 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
    945                 + " AND dataA." + Identity.NAMESPACE + " NOT NULL"
    946                 + " AND dataA." + Identity.IDENTITY + " NOT NULL"
    947                 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
    948                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
    949 
    950         final String[] COLUMNS = new String[] {
    951                 RawContactsColumns.CONCRETE_ID, RawContacts.CONTACT_ID,
    952                 RawContactsColumns.ACCOUNT_ID
    953         };
    954 
    955         int RAW_CONTACT_ID = 0;
    956         int CONTACT_ID = 1;
    957         int ACCOUNT_ID = 2;
    958     }
    959 
    960     protected interface NameLookupMatchQuery {
    961         String TABLE = Tables.NAME_LOOKUP + " nameA"
    962                 + " JOIN " + Tables.NAME_LOOKUP + " nameB" +
    963                 " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "="
    964                 + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")"
    965                 + " JOIN " + Tables.RAW_CONTACTS +
    966                 " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = "
    967                 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
    968 
    969         String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?"
    970                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
    971 
    972         String[] COLUMNS = new String[] {
    973                 RawContacts._ID,
    974                 RawContacts.CONTACT_ID,
    975                 RawContactsColumns.ACCOUNT_ID,
    976                 "nameA." + NameLookupColumns.NORMALIZED_NAME,
    977                 "nameA." + NameLookupColumns.NAME_TYPE,
    978                 "nameB." + NameLookupColumns.NAME_TYPE,
    979         };
    980 
    981         int RAW_CONTACT_ID = 0;
    982         int CONTACT_ID = 1;
    983         int ACCOUNT_ID = 2;
    984         int NAME = 3;
    985         int NAME_TYPE_A = 4;
    986         int NAME_TYPE_B = 5;
    987     }
    988 
    989     protected interface EmailLookupQuery {
    990         String TABLE = Tables.DATA + " dataA"
    991                 + " JOIN " + Tables.DATA + " dataB" +
    992                 " ON dataA." + Email.DATA + "= dataB." + Email.DATA
    993                 + " JOIN " + Tables.RAW_CONTACTS +
    994                 " ON (dataB." + Data.RAW_CONTACT_ID + " = "
    995                 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
    996 
    997         String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
    998                 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
    999                 + " AND dataA." + Email.DATA + " NOT NULL"
   1000                 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
   1001                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
   1002 
   1003         String[] COLUMNS = new String[] {
   1004                 Tables.RAW_CONTACTS + "." + RawContacts._ID,
   1005                 RawContacts.CONTACT_ID,
   1006                 RawContactsColumns.ACCOUNT_ID
   1007         };
   1008 
   1009         int RAW_CONTACT_ID = 0;
   1010         int CONTACT_ID = 1;
   1011         int ACCOUNT_ID = 2;
   1012     }
   1013 
   1014     protected interface PhoneLookupQuery {
   1015         String TABLE = Tables.PHONE_LOOKUP + " phoneA"
   1016                 + " JOIN " + Tables.DATA + " dataA"
   1017                 + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
   1018                 + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
   1019                 + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
   1020                 + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
   1021                 + " JOIN " + Tables.DATA + " dataB"
   1022                 + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
   1023                 + " JOIN " + Tables.RAW_CONTACTS
   1024                 + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
   1025                 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
   1026 
   1027         String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
   1028                 + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
   1029                 + "dataB." + Phone.NUMBER + ",?)"
   1030                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
   1031 
   1032         String[] COLUMNS = new String[] {
   1033                 Tables.RAW_CONTACTS + "." + RawContacts._ID,
   1034                 RawContacts.CONTACT_ID,
   1035                 RawContactsColumns.ACCOUNT_ID
   1036         };
   1037 
   1038         int RAW_CONTACT_ID = 0;
   1039         int CONTACT_ID = 1;
   1040         int ACCOUNT_ID = 2;
   1041     }
   1042 
   1043     protected interface NullNameRawContactsIdsQuery {
   1044         final String TABLE =  Tables.RAW_CONTACTS + " LEFT OUTER JOIN " +  Tables.NAME_LOOKUP
   1045                 + " ON "+ RawContacts._ID + " = " + NameLookupColumns.RAW_CONTACT_ID
   1046                 + " AND " + NameLookupColumns.NAME_TYPE + " = " + NameLookupType.NAME_EXACT;
   1047 
   1048         final String[] COLUMNS = new String[] {
   1049                 RawContacts._ID, RawContacts.CONTACT_ID, RawContactsColumns.ACCOUNT_ID,
   1050                 NameLookupColumns.NORMALIZED_NAME};
   1051 
   1052         int RAW_CONTACT_ID = 0;
   1053         int CONTACT_ID = 1;
   1054         int ACCOUNT_ID = 2;
   1055         int NAME = 3;
   1056     }
   1057 }
   1058