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