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