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