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.ArraySet;
35 import android.util.Log;
36 import com.android.providers.contacts.ContactsDatabaseHelper;
37 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
38 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
39 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
40 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
41 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
42 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
43 import com.android.providers.contacts.ContactsProvider2;
44 import com.android.providers.contacts.NameSplitter;
45 import com.android.providers.contacts.PhotoPriorityResolver;
46 import com.android.providers.contacts.TransactionContext;
47 import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
48 import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper;
49 import com.android.providers.contacts.aggregation.util.MatchScore;
50 import com.android.providers.contacts.aggregation.util.RawContactMatcher;
51 import com.android.providers.contacts.aggregation.util.RawContactMatchingCandidates;
52 import com.android.providers.contacts.database.ContactsTableUtil;
53 import com.google.android.collect.Sets;
54 import com.google.common.collect.HashMultimap;
55 import com.google.common.collect.Multimap;
56 
57 import java.util.ArrayList;
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      */
ContactAggregator2(ContactsProvider2 contactsProvider, ContactsDatabaseHelper contactsDatabaseHelper, PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter, CommonNicknameCache commonNicknameCache)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      */
aggregateContact(TransactionContext txContext, SQLiteDatabase db, long rawContactId, long accountId, long currentContactId, MatchCandidateList candidates)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 
needAggregate(SQLiteDatabase db, long rawContactId)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      */
findRawContactMatchingCandidates(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher)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 ArraySet<>();
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 ArraySet<>();
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      */
clearSuperPrimarySetting(SQLiteDatabase db, String rawContactIds)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 
buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2, int aggregationType, boolean countOnly)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      */
reAggregateRawContacts(TransactionContext txContext, SQLiteDatabase db, long currentCidForRawContact, long rawContactId, long accountId, long currentContactContentsCount, RawContactMatchingCandidates matchingCandidates)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 ArraySet<>();
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 ArraySet<>();
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      */
canBeReused(SQLiteDatabase db, Long contactId, Set<Long> connectedRawContactIds)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      */
breakComponentsByExceptions(SQLiteDatabase db, Set<Set<Long>> connectedRawContacts)423     private void breakComponentsByExceptions(SQLiteDatabase db,
424             Set<Set<Long>> connectedRawContacts) {
425         final Set<Set<Long>> tmpSets = new ArraySet<>(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      */
updateAggregationAfterVisibilityChange(long contactId)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      */
updateMatchScoresBasedOnExceptions(SQLiteDatabase db, long rawContactId, RawContactMatcher matcher)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      */
updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId, RawContactMatcher matcher)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      */
updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId, RawContactMatcher matcher)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 
updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId, RawContactMatcher matcher)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      */
updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query, MatchCandidateList candidates, RawContactMatcher matcher)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 
updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId, RawContactMatcher matcher)666     private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
667             RawContactMatcher matcher) {
668         Cursor c;
669         String useStrictPhoneNumberComparison =
670                 mDbHelper.getUseStrictPhoneNumberComparisonParameter();
671 
672         if (useStrictPhoneNumberComparison.equals("1")) {
673             mSelectionArgs2[0] = String.valueOf(rawContactId);
674             mSelectionArgs2[1] = useStrictPhoneNumberComparison;
675             c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
676                 PhoneLookupQuery.SELECTION,
677                 mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
678         } else {
679             mSelectionArgs3[0] = String.valueOf(rawContactId);
680             mSelectionArgs3[1] = useStrictPhoneNumberComparison;
681             mSelectionArgs3[2] = mDbHelper.getMinMatchParameter();
682             c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
683                 PhoneLookupQuery.SELECTION_MIN_MATCH,
684                 mSelectionArgs3, null, null, null, SECONDARY_HIT_LIMIT_STRING);
685         }
686 
687         try {
688             while (c.moveToNext()) {
689                 long rId = c.getLong(PhoneLookupQuery.RAW_CONTACT_ID);
690                 if (rId == rawContactId) {
691                     continue;
692                 }
693                 long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
694                 long accountId = c.getLong(PhoneLookupQuery.ACCOUNT_ID);
695                 matcher.updateScoreWithPhoneNumberMatch(rId, contactId, accountId);
696             }
697         } finally {
698             c.close();
699         }
700     }
701 
702     /**
703      * Loads name lookup rows for approximate name matching and updates match scores based on that
704      * data.
705      */
lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates, RawContactMatcher matcher)706     private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
707             RawContactMatcher matcher) {
708         ArraySet<String> firstLetters = new ArraySet<>();
709         for (int i = 0; i < candidates.mCount; i++) {
710             final NameMatchCandidate candidate = candidates.mList.get(i);
711             if (candidate.mName.length() >= 2) {
712                 String firstLetter = candidate.mName.substring(0, 2);
713                 if (!firstLetters.contains(firstLetter)) {
714                     firstLetters.add(firstLetter);
715                     final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '"
716                             + firstLetter + "*') AND "
717                             + "(" + NameLookupColumns.NAME_TYPE + " IN("
718                                     + NameLookupType.NAME_COLLATION_KEY + ","
719                                     + NameLookupType.EMAIL_BASED_NICKNAME + ","
720                                     + NameLookupType.NICKNAME + ")) AND "
721                             + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
722                     matchAllCandidates(db, selection, candidates, matcher,
723                             RawContactMatcher.MATCHING_ALGORITHM_APPROXIMATE,
724                             String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT));
725                 }
726             }
727         }
728     }
729 
730     private interface ContactNameLookupQuery {
731         String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
732 
733         String[] COLUMNS = new String[] {
734                 RawContacts._ID,
735                 RawContacts.CONTACT_ID,
736                 RawContactsColumns.ACCOUNT_ID,
737                 NameLookupColumns.NORMALIZED_NAME,
738                 NameLookupColumns.NAME_TYPE
739         };
740 
741         int RAW_CONTACT_ID = 0;
742         int CONTACT_ID = 1;
743         int ACCOUNT_ID = 2;
744         int NORMALIZED_NAME = 3;
745         int NAME_TYPE = 4;
746     }
747 
748     /**
749      * Loads all candidate rows from the name lookup table and updates match scores based
750      * on that data.
751      */
matchAllCandidates(SQLiteDatabase db, String selection, MatchCandidateList candidates, RawContactMatcher matcher, int algorithm, String limit)752     private void matchAllCandidates(SQLiteDatabase db, String selection,
753             MatchCandidateList candidates, RawContactMatcher matcher, int algorithm, String limit) {
754         final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
755                 selection, null, null, null, null, limit);
756 
757         try {
758             while (c.moveToNext()) {
759                 Long rawContactId = c.getLong(ContactNameLookupQuery.RAW_CONTACT_ID);
760                 Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
761                 Long accountId = c.getLong(ContactNameLookupQuery.ACCOUNT_ID);
762                 String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
763                 int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
764 
765                 // Note the N^2 complexity of the following fragment. This is not a huge concern
766                 // since the number of candidates is very small and in general secondary hits
767                 // in the absence of primary hits are rare.
768                 for (int i = 0; i < candidates.mCount; i++) {
769                     NameMatchCandidate candidate = candidates.mList.get(i);
770                     matcher.matchName(rawContactId, contactId, accountId, candidate.mLookupType,
771                             candidate.mName, nameType, name, algorithm);
772                 }
773             }
774         } finally {
775             c.close();
776         }
777     }
778 
779     private interface PhotoFileQuery {
780         final String[] COLUMNS = new String[] {
781                 PhotoFiles.HEIGHT,
782                 PhotoFiles.WIDTH,
783                 PhotoFiles.FILESIZE
784         };
785 
786         int HEIGHT = 0;
787         int WIDTH = 1;
788         int FILESIZE = 2;
789     }
790 
791     private class PhotoEntry implements Comparable<PhotoEntry> {
792         // Pixel count (width * height) for the image.
793         final int pixelCount;
794 
795         // File size (in bytes) of the image.  Not populated if the image is a thumbnail.
796         final int fileSize;
797 
PhotoEntry(int pixelCount, int fileSize)798         private PhotoEntry(int pixelCount, int fileSize) {
799             this.pixelCount = pixelCount;
800             this.fileSize = fileSize;
801         }
802 
803         @Override
compareTo(PhotoEntry pe)804         public int compareTo(PhotoEntry pe) {
805             if (pe == null) {
806                 return -1;
807             }
808             if (pixelCount == pe.pixelCount) {
809                 return pe.fileSize - fileSize;
810             } else {
811                 return pe.pixelCount - pixelCount;
812             }
813         }
814     }
815 
816     /**
817      * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
818      * descending order of match score.
819      * @param parameters
820      */
findMatchingContacts(final SQLiteDatabase db, long contactId, ArrayList<AggregationSuggestionParameter> parameters)821     protected List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId,
822             ArrayList<AggregationSuggestionParameter> parameters) {
823 
824         MatchCandidateList candidates = new MatchCandidateList();
825         RawContactMatcher matcher = new RawContactMatcher();
826 
827         if (parameters == null || parameters.size() == 0) {
828             final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
829                     RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
830             try {
831                 while (c.moveToNext()) {
832                     long rawContactId = c.getLong(RawContactIdQuery.RAW_CONTACT_ID);
833                     long accountId = c.getLong(RawContactIdQuery.ACCOUNT_ID);
834                     // Don't aggregate a contact with its own raw contacts.
835                     matcher.keepOut(rawContactId, contactId, accountId);
836                     updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
837                             matcher);
838                 }
839             } finally {
840                 c.close();
841             }
842         } else {
843             updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates,
844                     matcher, parameters);
845         }
846 
847         return matcher.pickBestMatches(SCORE_THRESHOLD_SUGGEST);
848     }
849 
850     /**
851      * Computes suggestion scores for contacts that have matching data rows.
852      * Aggregation suggestion doesn't consider aggregation exceptions, but is purely based on the
853      * raw contacts information.
854      */
updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher)855     private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
856             long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) {
857 
858         updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
859         updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
860         updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
861         updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
862         loadNameMatchCandidates(db, rawContactId, candidates, false);
863         lookupApproximateNameMatches(db, candidates, matcher);
864     }
865 
866     /**
867      * Computes scores for contacts that have matching data rows.
868      */
updateMatchScores(SQLiteDatabase db, long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher)869     private void updateMatchScores(SQLiteDatabase db, long rawContactId,
870             MatchCandidateList candidates, RawContactMatcher matcher) {
871         //update primary score
872         updateMatchScoresBasedOnExceptions(db, rawContactId, matcher);
873         updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
874         // update scores only if the raw contact doesn't have structured name
875         if (rawContactWithoutName(db, rawContactId)) {
876             updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
877             updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
878             updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
879             final List<Long> secondaryRawContactIds = matcher.prepareSecondaryMatchCandidates();
880             if (secondaryRawContactIds != null
881                     && secondaryRawContactIds.size() <= SECONDARY_HIT_LIMIT) {
882                 updateScoreForCandidatesWithoutName(db, secondaryRawContactIds, matcher);
883             }
884         }
885     }
886 
updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db, MatchCandidateList candidates, RawContactMatcher matcher, ArrayList<AggregationSuggestionParameter> parameters)887     private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
888             MatchCandidateList candidates, RawContactMatcher matcher,
889             ArrayList<AggregationSuggestionParameter> parameters) {
890         for (AggregationSuggestionParameter parameter : parameters) {
891             if (AggregationSuggestions.PARAMETER_MATCH_NAME.equals(parameter.kind)) {
892                 updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher);
893             }
894 
895             // TODO: add support for other parameter kinds
896         }
897     }
898 
rawContactWithoutName(SQLiteDatabase db, long rawContactId)899     private boolean rawContactWithoutName(SQLiteDatabase db, long rawContactId) {
900         String selection = RawContacts._ID + " =" + rawContactId;
901         final Cursor c = db.query(NullNameRawContactsIdsQuery.TABLE,
902                 NullNameRawContactsIdsQuery.COLUMNS, selection, null, null, null, null);
903 
904         try {
905             if (c.moveToFirst()) {
906                 return TextUtils.isEmpty(c.getString(NullNameRawContactsIdsQuery.NAME));
907             }
908         } finally {
909             c.close();
910         }
911         return false;
912     }
913 
914     /**
915      * Update scores for matches with secondary data matching but no structured name.
916      */
updateScoreForCandidatesWithoutName(SQLiteDatabase db, List<Long> secondaryRawContactIds, RawContactMatcher matcher)917     private void updateScoreForCandidatesWithoutName(SQLiteDatabase db,
918             List<Long> secondaryRawContactIds, RawContactMatcher matcher) {
919 
920         mSb.setLength(0);
921 
922         mSb.append(RawContacts._ID).append(" IN (");
923         for (int i = 0; i < secondaryRawContactIds.size(); i++) {
924             if (i != 0) {
925                 mSb.append(",");
926             }
927             mSb.append(secondaryRawContactIds.get(i));
928         }
929         mSb.append( ")");
930         final Cursor c = db.query(NullNameRawContactsIdsQuery.TABLE,
931                 NullNameRawContactsIdsQuery.COLUMNS, mSb.toString(), null, null, null, null);
932 
933         try {
934             while (c.moveToNext()) {
935                 Long rId = c.getLong(NullNameRawContactsIdsQuery.RAW_CONTACT_ID);
936                 Long contactId = c.getLong(NullNameRawContactsIdsQuery.CONTACT_ID);
937                 Long accountId = c.getLong(NullNameRawContactsIdsQuery.ACCOUNT_ID);
938                 String name = c.getString(NullNameRawContactsIdsQuery.NAME);
939                 if (TextUtils.isEmpty(name)) {
940                     matcher.matchNoName(rId, contactId, accountId);
941                 }
942             }
943         } finally {
944             c.close();
945         }
946     }
947 
948     protected interface IdentityLookupMatchQuery {
949         final String TABLE = Tables.DATA + " dataA"
950                 + " JOIN " + Tables.DATA + " dataB" +
951                 " ON (dataA." + Identity.NAMESPACE + "=dataB." + Identity.NAMESPACE +
952                 " AND dataA." + Identity.IDENTITY + "=dataB." + Identity.IDENTITY + ")"
953                 + " JOIN " + Tables.RAW_CONTACTS +
954                 " ON (dataB." + Data.RAW_CONTACT_ID + " = "
955                 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
956 
957         final String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
958                 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
959                 + " AND dataA." + Identity.NAMESPACE + " NOT NULL"
960                 + " AND dataA." + Identity.IDENTITY + " NOT NULL"
961                 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
962                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
963 
964         final String[] COLUMNS = new String[] {
965                 RawContactsColumns.CONCRETE_ID, RawContacts.CONTACT_ID,
966                 RawContactsColumns.ACCOUNT_ID
967         };
968 
969         int RAW_CONTACT_ID = 0;
970         int CONTACT_ID = 1;
971         int ACCOUNT_ID = 2;
972     }
973 
974     protected interface NameLookupMatchQuery {
975         String TABLE = Tables.NAME_LOOKUP + " nameA"
976                 + " JOIN " + Tables.NAME_LOOKUP + " nameB" +
977                 " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "="
978                 + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")"
979                 + " JOIN " + Tables.RAW_CONTACTS +
980                 " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = "
981                 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
982 
983         String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?"
984                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
985 
986         String[] COLUMNS = new String[] {
987                 RawContacts._ID,
988                 RawContacts.CONTACT_ID,
989                 RawContactsColumns.ACCOUNT_ID,
990                 "nameA." + NameLookupColumns.NORMALIZED_NAME,
991                 "nameA." + NameLookupColumns.NAME_TYPE,
992                 "nameB." + NameLookupColumns.NAME_TYPE,
993         };
994 
995         int RAW_CONTACT_ID = 0;
996         int CONTACT_ID = 1;
997         int ACCOUNT_ID = 2;
998         int NAME = 3;
999         int NAME_TYPE_A = 4;
1000         int NAME_TYPE_B = 5;
1001     }
1002 
1003     protected interface EmailLookupQuery {
1004         String TABLE = Tables.DATA + " dataA"
1005                 + " JOIN " + Tables.DATA + " dataB" +
1006                 " ON dataA." + Email.DATA + "= dataB." + Email.DATA
1007                 + " JOIN " + Tables.RAW_CONTACTS +
1008                 " ON (dataB." + Data.RAW_CONTACT_ID + " = "
1009                 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1010 
1011         String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
1012                 + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
1013                 + " AND dataA." + Email.DATA + " NOT NULL"
1014                 + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
1015                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1016 
1017         String[] COLUMNS = new String[] {
1018                 Tables.RAW_CONTACTS + "." + RawContacts._ID,
1019                 RawContacts.CONTACT_ID,
1020                 RawContactsColumns.ACCOUNT_ID
1021         };
1022 
1023         int RAW_CONTACT_ID = 0;
1024         int CONTACT_ID = 1;
1025         int ACCOUNT_ID = 2;
1026     }
1027 
1028     protected interface PhoneLookupQuery {
1029         String TABLE = Tables.PHONE_LOOKUP + " phoneA"
1030                 + " JOIN " + Tables.DATA + " dataA"
1031                 + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
1032                 + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
1033                 + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
1034                 + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
1035                 + " JOIN " + Tables.DATA + " dataB"
1036                 + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
1037                 + " JOIN " + Tables.RAW_CONTACTS
1038                 + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
1039                 + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1040 
1041         String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
1042                 + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
1043                 + "dataB." + Phone.NUMBER + ",?)"
1044                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1045 
1046         String SELECTION_MIN_MATCH = "dataA." + Data.RAW_CONTACT_ID + "=?"
1047                 + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
1048                 + "dataB." + Phone.NUMBER + ",?,?)"
1049                 + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1050 
1051         String[] COLUMNS = new String[] {
1052                 Tables.RAW_CONTACTS + "." + RawContacts._ID,
1053                 RawContacts.CONTACT_ID,
1054                 RawContactsColumns.ACCOUNT_ID
1055         };
1056 
1057         int RAW_CONTACT_ID = 0;
1058         int CONTACT_ID = 1;
1059         int ACCOUNT_ID = 2;
1060     }
1061 
1062     protected interface NullNameRawContactsIdsQuery {
1063         final String TABLE =  Tables.RAW_CONTACTS + " LEFT OUTER JOIN " +  Tables.NAME_LOOKUP
1064                 + " ON "+ RawContacts._ID + " = " + NameLookupColumns.RAW_CONTACT_ID
1065                 + " AND " + NameLookupColumns.NAME_TYPE + " = " + NameLookupType.NAME_EXACT;
1066 
1067         final String[] COLUMNS = new String[] {
1068                 RawContacts._ID, RawContacts.CONTACT_ID, RawContactsColumns.ACCOUNT_ID,
1069                 NameLookupColumns.NORMALIZED_NAME};
1070 
1071         int RAW_CONTACT_ID = 0;
1072         int CONTACT_ID = 1;
1073         int ACCOUNT_ID = 2;
1074         int NAME = 3;
1075     }
1076 }
1077