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