ContactAggregator2.java revision 0ef6fe74c10e27d2e1b602a81a52f9c4053fdb03
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
17package com.android.providers.contacts.aggregation;
18
19import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_PRIMARY;
20import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_SECONDARY;
21import static com.android.providers.contacts.aggregation.util.RawContactMatcher.SCORE_THRESHOLD_SUGGEST;
22import android.database.Cursor;
23import android.database.sqlite.SQLiteDatabase;
24import android.provider.ContactsContract.AggregationExceptions;
25import android.provider.ContactsContract.CommonDataKinds.Email;
26import android.provider.ContactsContract.CommonDataKinds.Identity;
27import android.provider.ContactsContract.CommonDataKinds.Phone;
28import android.provider.ContactsContract.Contacts.AggregationSuggestions;
29import android.provider.ContactsContract.Data;
30import android.provider.ContactsContract.FullNameStyle;
31import android.provider.ContactsContract.PhotoFiles;
32import android.provider.ContactsContract.RawContacts;
33import android.text.TextUtils;
34import android.util.Log;
35import com.android.providers.contacts.ContactsDatabaseHelper;
36import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
37import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
38import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
39import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
40import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
41import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
42import com.android.providers.contacts.ContactsProvider2;
43import com.android.providers.contacts.NameSplitter;
44import com.android.providers.contacts.PhotoPriorityResolver;
45import com.android.providers.contacts.TransactionContext;
46import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
47import com.android.providers.contacts.aggregation.util.ContactAggregatorHelper;
48import com.android.providers.contacts.aggregation.util.MatchScore;
49import com.android.providers.contacts.aggregation.util.RawContactMatcher;
50import com.android.providers.contacts.aggregation.util.RawContactMatchingCandidates;
51import com.android.providers.contacts.database.ContactsTableUtil;
52import com.google.android.collect.Sets;
53import com.google.common.collect.HashMultimap;
54import com.google.common.collect.Multimap;
55
56import java.util.ArrayList;
57import java.util.HashSet;
58import java.util.List;
59import java.util.Map;
60import java.util.Set;
61
62/**
63 * ContactAggregator2 deals with aggregating contact information with sufficient matching data
64 * points. E.g., two John Doe contacts with same phone numbers are presumed to be the same
65 * person unless the user declares otherwise.
66 */
67public class ContactAggregator2 extends AbstractContactAggregator {
68
69    // Possible operation types for contacts aggregation.
70    private static final int CREATE_NEW_CONTACT = 1;
71    private static final int KEEP_INTACT = 0;
72    private static final int RE_AGGREGATE = -1;
73
74    private final RawContactMatcher mMatcher = new RawContactMatcher();
75
76    /**
77     * Constructor.
78     */
79    public ContactAggregator2(ContactsProvider2 contactsProvider,
80            ContactsDatabaseHelper contactsDatabaseHelper,
81            PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter,
82            CommonNicknameCache commonNicknameCache) {
83        super(contactsProvider, contactsDatabaseHelper, photoPriorityResolver, nameSplitter,
84                commonNicknameCache);
85    }
86
87    /**
88     * Given a specific raw contact, finds all matching raw contacts and re-aggregate them
89     * based on the matching connectivity.
90     */
91     synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db,
92             long rawContactId, long accountId, long currentContactId,
93             MatchCandidateList candidates) {
94
95         if (!needAggregate(db, rawContactId)) {
96             if (VERBOSE_LOGGING) {
97                 Log.v(TAG, "Skip rid=" + rawContactId + " which has already been aggregated.");
98             }
99             return;
100         }
101
102         if (VERBOSE_LOGGING) {
103            Log.v(TAG, "aggregateContact: rid=" + rawContactId + " cid=" + currentContactId);
104        }
105
106        int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
107
108        Integer aggModeObject = mRawContactsMarkedForAggregation.remove(rawContactId);
109        if (aggModeObject != null) {
110            aggregationMode = aggModeObject;
111        }
112
113        RawContactMatcher matcher = new RawContactMatcher();
114        RawContactMatchingCandidates matchingCandidates = new RawContactMatchingCandidates();
115        if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
116            // If this is a newly inserted contact or a visible contact, look for
117            // data matches.
118            if (currentContactId == 0
119                    || mDbHelper.isContactInDefaultDirectory(db, currentContactId)) {
120                // Find the set of matching candidates
121                matchingCandidates = findRawContactMatchingCandidates(db, rawContactId, candidates,
122                        matcher);
123            }
124        } else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
125            return;
126        }
127
128        // # of raw_contacts in the [currentContactId] contact excluding the [rawContactId]
129        // raw_contact.
130        long currentContactContentsCount = 0;
131
132        if (currentContactId != 0) {
133            mRawContactCountQuery.bindLong(1, currentContactId);
134            mRawContactCountQuery.bindLong(2, rawContactId);
135            currentContactContentsCount = mRawContactCountQuery.simpleQueryForLong();
136        }
137
138        // Set aggregation operation, i.e., re-aggregate, keep intact, or create new contact based
139        // on the number of matching candidates and the number of raw_contacts in the
140        // [currentContactId] excluding the [rawContactId].
141        final int operation;
142        final int candidatesCount = matchingCandidates.getCount();
143        if (candidatesCount >= AGGREGATION_CONTACT_SIZE_LIMIT) {
144            operation = KEEP_INTACT;
145            if (VERBOSE_LOGGING) {
146                Log.v(TAG, "Too many matching raw contacts (" + candidatesCount
147                        + ") are found, so skip aggregation");
148            }
149        } else if (candidatesCount > 0) {
150            operation = RE_AGGREGATE;
151        } else {
152            // When there is no matching raw contact found, if there are no other raw contacts in
153            // the current aggregate, we might as well reuse it. Also, if the aggregation mode is
154            // SUSPENDED, we must reuse the same aggregate.
155            if (currentContactId != 0
156                    && (currentContactContentsCount == 0
157                    || aggregationMode == RawContacts.AGGREGATION_MODE_SUSPENDED)) {
158                operation = KEEP_INTACT;
159            } else {
160                operation = CREATE_NEW_CONTACT;
161            }
162        }
163
164        if (operation == KEEP_INTACT) {
165            // Aggregation unchanged
166            if (VERBOSE_LOGGING) {
167                Log.v(TAG, "Aggregation unchanged");
168            }
169            markAggregated(db, String.valueOf(rawContactId));
170        } else if (operation == CREATE_NEW_CONTACT) {
171            // create new contact for [rawContactId]
172            if (VERBOSE_LOGGING) {
173                Log.v(TAG, "create new contact for rid=" + rawContactId);
174            }
175            createContactForRawContacts(db, txContext, Sets.newHashSet(rawContactId), null);
176            if (currentContactContentsCount > 0) {
177                updateAggregateData(txContext, currentContactId);
178            }
179            markAggregated(db, String.valueOf(rawContactId));
180        } else {
181            // re-aggregate
182            if (VERBOSE_LOGGING) {
183                Log.v(TAG, "Re-aggregating rids=" + rawContactId + ","
184                        + TextUtils.join(",", matchingCandidates.getRawContactIdSet()));
185            }
186            reAggregateRawContacts(txContext, db, currentContactId, rawContactId, accountId,
187                    currentContactContentsCount, matchingCandidates);
188        }
189    }
190
191    private boolean needAggregate(SQLiteDatabase db, long rawContactId) {
192        final String sql = "SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS +
193                " WHERE " + RawContactsColumns.AGGREGATION_NEEDED + "=1" +
194                " AND " + RawContacts._ID + "=?";
195
196        mSelectionArgs1[0] = String.valueOf(rawContactId);
197        final Cursor cursor = db.rawQuery(sql, mSelectionArgs1);
198
199        try {
200            return cursor.getCount() != 0;
201        } finally {
202            cursor.close();
203        }
204    }
205    /**
206     * Find the set of matching raw contacts for given rawContactId. Add all the raw contact
207     * candidates with matching scores > threshold to RawContactMatchingCandidates. Keep doing
208     * this for every raw contact in RawContactMatchingCandidates until is it not changing.
209     */
210    private RawContactMatchingCandidates findRawContactMatchingCandidates(SQLiteDatabase db, long
211            rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) {
212        updateMatchScores(db, rawContactId, candidates, matcher);
213        final RawContactMatchingCandidates matchingCandidates = new RawContactMatchingCandidates(
214                matcher.pickBestMatches());
215        Set<Long> newIds = new HashSet<>();
216        newIds.addAll(matchingCandidates.getRawContactIdSet());
217        // Keep doing the following until no new raw contact candidate is found.
218        while (!newIds.isEmpty()) {
219            if (matchingCandidates.getCount() >= AGGREGATION_CONTACT_SIZE_LIMIT) {
220                return matchingCandidates;
221            }
222            final Set<Long> tmpIdSet = new HashSet<>();
223            for (long rId : newIds) {
224                final RawContactMatcher rMatcher = new RawContactMatcher();
225                updateMatchScores(db, rId, new MatchCandidateList(),
226                        rMatcher);
227                List<MatchScore> newMatches = rMatcher.pickBestMatches();
228                for (MatchScore newMatch : newMatches) {
229                    final long newRawContactId = newMatch.getRawContactId();
230                    if (!matchingCandidates.getRawContactIdSet().contains(newRawContactId)) {
231                        tmpIdSet.add(newRawContactId);
232                        matchingCandidates.add(newMatch);
233                    }
234                }
235            }
236            newIds.clear();
237            newIds.addAll(tmpIdSet);
238        }
239        return matchingCandidates;
240    }
241
242    /**
243     * Find out which mime-types are shared by more than one contacts for {@code rawContactIds}.
244     * Clear the is_super_primary settings for these mime-types.
245     * {@code rawContactIds} should be a comma separated ID list.
246     */
247     private void clearSuperPrimarySetting(SQLiteDatabase db, String rawContactIds) {
248        final String sql =
249                "SELECT " + DataColumns.MIMETYPE_ID + ", count(1) c  FROM " +
250                        Tables.DATA +" WHERE " + Data.IS_SUPER_PRIMARY + " = 1 AND " +
251                        Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ") group by " +
252                        DataColumns.MIMETYPE_ID + " HAVING c > 1";
253
254        // Find out which mime-types exist with is_super_primary=true on more then one contacts.
255        int index = 0;
256        final StringBuilder mimeTypeCondition = new StringBuilder();
257        mimeTypeCondition.append(" AND " + DataColumns.MIMETYPE_ID + " IN (");
258
259        final Cursor c = db.rawQuery(sql, null);
260        try {
261            c.moveToPosition(-1);
262            while (c.moveToNext()) {
263                if (index > 0) {
264                    mimeTypeCondition.append(',');
265                }
266                mimeTypeCondition.append(c.getLong((0)));
267                index++;
268            }
269        } finally {
270            c.close();
271        }
272
273        if (index == 0) {
274            return;
275        }
276
277        // Clear is_super_primary setting for all the mime-types with is_super_primary=true
278        // in both raw contact of rawContactId and raw contacts of contactId
279        String superPrimaryUpdateSql = "UPDATE " + Tables.DATA +
280                " SET " + Data.IS_SUPER_PRIMARY + "=0" +
281                " WHERE " +  Data.RAW_CONTACT_ID +
282                " IN (" + rawContactIds + ")";
283
284        mimeTypeCondition.append(')');
285        superPrimaryUpdateSql += mimeTypeCondition.toString();
286        db.execSQL(superPrimaryUpdateSql);
287    }
288
289    private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
290            int aggregationType, boolean countOnly) {
291        final String idPairSelection =  "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " +
292                AggregationExceptions.RAW_CONTACT_ID2;
293        final String sql =
294                " FROM " + Tables.AGGREGATION_EXCEPTIONS +
295                " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" +
296                        rawContactIdSet1 + ")" +
297                " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" +
298                " AND " + AggregationExceptions.TYPE + "=" + aggregationType;
299        return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
300                idPairSelection + sql;
301    }
302
303    /**
304     * Re-aggregate rawContact of {@code rawContactId} and all the raw contacts of
305     * {@code matchingCandidates} into connected components. This only happens when a given
306     * raw contacts cannot be joined with its best matching contacts directly.
307     *
308     *  Two raw contacts are considered connected if they share at least one email address, phone
309     *  number or identity. Create new contact for each connected component except the very first
310     *  one that doesn't contain rawContactId of {@code rawContactId}.
311     */
312    private void reAggregateRawContacts(TransactionContext txContext, SQLiteDatabase db,
313            long currentCidForRawContact, long rawContactId, long accountId,
314            long currentContactContentsCount, RawContactMatchingCandidates matchingCandidates) {
315        // Find the connected component based on the aggregation exceptions or
316        // identity/email/phone matching for all the raw contacts of [contactId] and the give
317        // raw contact.
318        final Set<Long> allIds = new HashSet<>();
319        allIds.add(rawContactId);
320        allIds.addAll(matchingCandidates.getRawContactIdSet());
321        final Set<Set<Long>> connectedRawContactSets = findConnectedRawContacts(db, allIds);
322
323        final Map<Long, Long> rawContactsToAccounts = matchingCandidates.getRawContactToAccount();
324        rawContactsToAccounts.put(rawContactId, accountId);
325        ContactAggregatorHelper.mergeComponentsWithDisjointAccounts(connectedRawContactSets,
326                rawContactsToAccounts);
327        breakComponentsByExceptions(db, connectedRawContactSets);
328
329        // Create new contact for each connected component. Use the first reusable contactId if
330        // possible. If no reusable contactId found, create new contact for the connected component.
331        // Update aggregate data for all the contactIds touched by this connected component,
332        for (Set<Long> connectedRawContactIds : connectedRawContactSets) {
333            Long contactId = null;
334            Set<Long> cidsNeedToBeUpdated = new HashSet<>();
335            if (connectedRawContactIds.contains(rawContactId)) {
336                // If there is no other raw contacts aggregated with the given raw contact currently
337                // or all the raw contacts in [currentCidForRawContact] are still in the same
338                // connected component, we might as well reuse it.
339                if (currentCidForRawContact != 0 &&
340                        (currentContactContentsCount == 0) ||
341                        canBeReused(db, currentCidForRawContact, connectedRawContactIds)) {
342                    contactId = currentCidForRawContact;
343                    for (Long connectedRawContactId : connectedRawContactIds) {
344                        Long cid = matchingCandidates.getContactId(connectedRawContactId);
345                        if (cid != null && !cid.equals(contactId)) {
346                            cidsNeedToBeUpdated.add(cid);
347                        }
348                    }
349                } else if (currentCidForRawContact != 0){
350                    cidsNeedToBeUpdated.add(currentCidForRawContact);
351                }
352            } else {
353                boolean foundContactId = false;
354                for (Long connectedRawContactId : connectedRawContactIds) {
355                    Long currentContactId = matchingCandidates.getContactId(connectedRawContactId);
356                    if (!foundContactId && currentContactId != null &&
357                            canBeReused(db, currentContactId, connectedRawContactIds)) {
358                        contactId = currentContactId;
359                        foundContactId = true;
360                    } else {
361                        cidsNeedToBeUpdated.add(currentContactId);
362                    }
363                }
364            }
365            final String connectedRids = TextUtils.join(",", connectedRawContactIds);
366            clearSuperPrimarySetting(db, connectedRids);
367            createContactForRawContacts(db, txContext, connectedRawContactIds, contactId);
368            // re-aggregate
369            if (VERBOSE_LOGGING) {
370                Log.v(TAG, "Aggregating rids=" + connectedRawContactIds);
371            }
372            markAggregated(db, connectedRids);
373
374            for (Long cid : cidsNeedToBeUpdated) {
375                long currentRcCount = 0;
376                if (cid != 0) {
377                    mRawContactCountQuery.bindLong(1, cid);
378                    mRawContactCountQuery.bindLong(2, 0);
379                    currentRcCount = mRawContactCountQuery.simpleQueryForLong();
380                }
381
382                if (currentRcCount == 0) {
383                    // Delete a contact if it doesn't contain anything
384                    ContactsTableUtil.deleteContact(db, cid);
385                    mAggregatedPresenceDelete.bindLong(1, cid);
386                    mAggregatedPresenceDelete.execute();
387                } else {
388                    updateAggregateData(txContext, cid);
389                }
390            }
391        }
392    }
393
394    /**
395     * Check if contactId can be reused as the contact Id for new aggregation of all the
396     * connectedRawContactIds. If connectedRawContactIds set contains all the raw contacts
397     * currently aggregated under contactId, return true; Otherwise, return false.
398     */
399    private boolean canBeReused(SQLiteDatabase db, Long contactId,
400            Set<Long> connectedRawContactIds) {
401        final String sql = "SELECT " + RawContactsColumns.CONCRETE_ID + " FROM " +
402                Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=? AND " +
403                RawContacts.DELETED + "=0";
404        mSelectionArgs1[0] = String.valueOf(contactId);
405        final Cursor cursor = db.rawQuery(sql, mSelectionArgs1);
406        try {
407            cursor.moveToPosition(-1);
408            while (cursor.moveToNext()) {
409                if (!connectedRawContactIds.contains(cursor.getLong(0))) {
410                    return false;
411                }
412            }
413        } finally {
414            cursor.close();
415        }
416        return true;
417    }
418
419    /**
420     * Separate all the raw_contacts which has "SEPARATE" aggregation exception to another
421     * raw_contacts in the same component.
422     */
423    private void breakComponentsByExceptions(SQLiteDatabase db,
424            Set<Set<Long>> connectedRawContacts) {
425        final Set<Set<Long>> tmpSets = new HashSet<>(connectedRawContacts);
426        for (Set<Long> component : tmpSets) {
427            final String rawContacts = TextUtils.join(",", component);
428            // If "SEPARATE" exception is found inside an connected component [component],
429            // remove the [component] from [connectedRawContacts], and create new connected
430            // components for all raw contacts of [component] solely based on "JOIN" exceptions
431            // and add them to [connectedRawContacts].
432            if (isFirstColumnGreaterThanZero(db, buildExceptionMatchingSql(rawContacts, rawContacts,
433                    AggregationExceptions.TYPE_KEEP_SEPARATE, /* countOnly =*/true))) {
434                Multimap<Long, Long> joinPairs = HashMultimap.create();
435                findIdPairs(db, buildExceptionMatchingSql(rawContacts, rawContacts), joinPairs);
436                connectedRawContacts.remove(component);
437                connectedRawContacts.addAll(
438                    ContactAggregatorHelper.findConnectedComponents(component, joinPairs));
439            }
440        }
441    }
442
443    /**
444     * Ensures that automatic aggregation rules are followed after a contact
445     * becomes visible or invisible. Specifically, consider this case: there are
446     * three contacts named Foo. Two of them come from account A1 and one comes
447     * from account A2. The aggregation rules say that in this case none of the
448     * three Foo's should be aggregated: two of them are in the same account, so
449     * they don't get aggregated; the third has two affinities, so it does not
450     * join either of them.
451     * <p>
452     * Consider what happens if one of the "Foo"s from account A1 becomes
453     * invisible. Nothing stands in the way of aggregating the other two
454     * anymore, so they should get joined.
455     * <p>
456     * What if the invisible "Foo" becomes visible after that? We should split the
457     * aggregate between the other two.
458     */
459    public void updateAggregationAfterVisibilityChange(long contactId) {
460        SQLiteDatabase db = mDbHelper.getWritableDatabase();
461        boolean visible = mDbHelper.isContactInDefaultDirectory(db, contactId);
462        if (visible) {
463            markContactForAggregation(db, contactId);
464        } else {
465            // Find all contacts that _could be_ aggregated with this one and
466            // rerun aggregation for all of them
467            mSelectionArgs1[0] = String.valueOf(contactId);
468            Cursor cursor = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
469                    RawContactIdQuery.SELECTION, mSelectionArgs1, null, null, null);
470            try {
471                while (cursor.moveToNext()) {
472                    long rawContactId = cursor.getLong(RawContactIdQuery.RAW_CONTACT_ID);
473                    mMatcher.clear();
474
475                    updateMatchScoresBasedOnIdentityMatch(db, rawContactId, mMatcher);
476                    updateMatchScoresBasedOnNameMatches(db, rawContactId, mMatcher);
477                    List<MatchScore> bestMatches =
478                            mMatcher.pickBestMatches(SCORE_THRESHOLD_PRIMARY);
479                    for (MatchScore matchScore : bestMatches) {
480                        markContactForAggregation(db, matchScore.getContactId());
481                    }
482
483                    mMatcher.clear();
484                    updateMatchScoresBasedOnEmailMatches(db, rawContactId, mMatcher);
485                    updateMatchScoresBasedOnPhoneMatches(db, rawContactId, mMatcher);
486                    bestMatches =
487                            mMatcher.pickBestMatches(SCORE_THRESHOLD_SECONDARY);
488                    for (MatchScore matchScore : bestMatches) {
489                        markContactForAggregation(db, matchScore.getContactId());
490                    }
491                }
492            } finally {
493                cursor.close();
494            }
495        }
496    }
497
498    /**
499     * Computes match scores based on exceptions entered by the user: always match and never match.
500     */
501    private void updateMatchScoresBasedOnExceptions(SQLiteDatabase db, long rawContactId,
502            RawContactMatcher matcher) {
503        if (!mAggregationExceptionIdsValid) {
504            prefetchAggregationExceptionIds(db);
505        }
506
507        // If there are no aggregation exceptions involving this raw contact, there is no need to
508        // run a query and we can just return -1, which stands for "nothing found"
509        if (!mAggregationExceptionIds.contains(rawContactId)) {
510            return;
511        }
512
513        final Cursor c = db.query(AggregateExceptionQuery.TABLE,
514                AggregateExceptionQuery.COLUMNS,
515                AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId
516                        + " OR " + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId,
517                null, null, null, null);
518
519        try {
520            while (c.moveToNext()) {
521                int type = c.getInt(AggregateExceptionQuery.TYPE);
522                long rawContactId1 = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1);
523                long contactId = -1;
524                long rId = -1;
525                long accountId = -1;
526                if (rawContactId == rawContactId1) {
527                    if (!c.isNull(AggregateExceptionQuery.RAW_CONTACT_ID2)) {
528                        rId = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID2);
529                        contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID2);
530                        accountId = c.getLong(AggregateExceptionQuery.ACCOUNT_ID2);
531                    }
532                } else {
533                    if (!c.isNull(AggregateExceptionQuery.RAW_CONTACT_ID1)) {
534                        rId = c.getLong(AggregateExceptionQuery.RAW_CONTACT_ID1);
535                        contactId = c.getLong(AggregateExceptionQuery.CONTACT_ID1);
536                        accountId = c.getLong(AggregateExceptionQuery.ACCOUNT_ID1);
537                    }
538                }
539                if (rId != -1) {
540                    if (type == AggregationExceptions.TYPE_KEEP_TOGETHER) {
541                        matcher.keepIn(rId, contactId, accountId);
542                    } else {
543                        matcher.keepOut(rId, contactId, accountId);
544                    }
545                }
546            }
547        } finally {
548            c.close();
549        }
550    }
551
552    /**
553     * Finds contacts with exact identity matches to the the specified raw contact.
554     */
555    private void updateMatchScoresBasedOnIdentityMatch(SQLiteDatabase db, long rawContactId,
556            RawContactMatcher matcher) {
557        mSelectionArgs2[0] = String.valueOf(rawContactId);
558        mSelectionArgs2[1] = String.valueOf(mMimeTypeIdIdentity);
559        Cursor c = db.query(IdentityLookupMatchQuery.TABLE, IdentityLookupMatchQuery.COLUMNS,
560                IdentityLookupMatchQuery.SELECTION,
561                mSelectionArgs2, RawContacts.CONTACT_ID, null, null);
562        try {
563            while (c.moveToNext()) {
564                final long rId = c.getLong(IdentityLookupMatchQuery.RAW_CONTACT_ID);
565                if (rId == rawContactId) {
566                    continue;
567                }
568                final long contactId = c.getLong(IdentityLookupMatchQuery.CONTACT_ID);
569                final long accountId = c.getLong(IdentityLookupMatchQuery.ACCOUNT_ID);
570                matcher.matchIdentity(rId, contactId, accountId);
571            }
572        } finally {
573            c.close();
574        }
575    }
576
577    /**
578     * Finds contacts with names matching the name of the specified raw contact.
579     */
580    private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
581            RawContactMatcher matcher) {
582        mSelectionArgs1[0] = String.valueOf(rawContactId);
583        Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS,
584                NameLookupMatchQuery.SELECTION,
585                mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING);
586        try {
587            while (c.moveToNext()) {
588                long rId =  c.getLong(NameLookupMatchQuery.RAW_CONTACT_ID);
589                if (rId == rawContactId) {
590                    continue;
591                }
592                long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID);
593                long accountId = c.getLong(NameLookupMatchQuery.ACCOUNT_ID);
594                String name = c.getString(NameLookupMatchQuery.NAME);
595                int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A);
596                int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B);
597                matcher.matchName(rId, contactId, accountId, nameTypeA, name,
598                        nameTypeB, name, RawContactMatcher.MATCHING_ALGORITHM_EXACT);
599                if (nameTypeA == NameLookupType.NICKNAME &&
600                        nameTypeB == NameLookupType.NICKNAME) {
601                    matcher.updateScoreWithNicknameMatch(rId, contactId, accountId);
602                }
603            }
604        } finally {
605            c.close();
606        }
607    }
608
609    private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
610            RawContactMatcher matcher) {
611        mSelectionArgs2[0] = String.valueOf(rawContactId);
612        mSelectionArgs2[1] = String.valueOf(mMimeTypeIdEmail);
613        Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
614                EmailLookupQuery.SELECTION,
615                mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
616        try {
617            while (c.moveToNext()) {
618                long rId = c.getLong(EmailLookupQuery.RAW_CONTACT_ID);
619                if (rId == rawContactId) {
620                    continue;
621                }
622                long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
623                long accountId = c.getLong(EmailLookupQuery.ACCOUNT_ID);
624                matcher.updateScoreWithEmailMatch(rId, contactId, accountId);
625            }
626        } finally {
627            c.close();
628        }
629    }
630
631    /**
632     * Finds contacts with names matching the specified name.
633     */
634    private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query,
635            MatchCandidateList candidates, RawContactMatcher matcher) {
636        candidates.clear();
637        NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder(
638                mNameSplitter, candidates);
639        builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED);
640        if (builder.isEmpty()) {
641            return;
642        }
643
644        Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE,
645                NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null,
646                null, PRIMARY_HIT_LIMIT_STRING);
647        try {
648            while (c.moveToNext()) {
649                long rId = c.getLong(NameLookupMatchQueryWithParameter.RAW_CONTACT_ID);
650                long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID);
651                long accountId = c.getLong(NameLookupMatchQueryWithParameter.ACCOUNT_ID);
652                String name = c.getString(NameLookupMatchQueryWithParameter.NAME);
653                int nameTypeA = builder.getLookupType(name);
654                int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE);
655                matcher.matchName(rId, contactId, accountId, nameTypeA, name, nameTypeB, name,
656                        RawContactMatcher.MATCHING_ALGORITHM_EXACT);
657                if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) {
658                    matcher.updateScoreWithNicknameMatch(rId, contactId, accountId);
659                }
660            }
661        } finally {
662            c.close();
663        }
664    }
665
666    private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
667            RawContactMatcher matcher) {
668        mSelectionArgs2[0] = String.valueOf(rawContactId);
669        mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter();
670        Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
671                PhoneLookupQuery.SELECTION,
672                mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
673        try {
674            while (c.moveToNext()) {
675                long rId = c.getLong(PhoneLookupQuery.RAW_CONTACT_ID);
676                if (rId == rawContactId) {
677                    continue;
678                }
679                long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
680                long accountId = c.getLong(PhoneLookupQuery.ACCOUNT_ID);
681                matcher.updateScoreWithPhoneNumberMatch(rId, contactId, accountId);
682            }
683        } finally {
684            c.close();
685        }
686    }
687
688    /**
689     * Loads name lookup rows for approximate name matching and updates match scores based on that
690     * data.
691     */
692    private void lookupApproximateNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
693            RawContactMatcher matcher) {
694        HashSet<String> firstLetters = new HashSet<>();
695        for (int i = 0; i < candidates.mCount; i++) {
696            final NameMatchCandidate candidate = candidates.mList.get(i);
697            if (candidate.mName.length() >= 2) {
698                String firstLetter = candidate.mName.substring(0, 2);
699                if (!firstLetters.contains(firstLetter)) {
700                    firstLetters.add(firstLetter);
701                    final String selection = "(" + NameLookupColumns.NORMALIZED_NAME + " GLOB '"
702                            + firstLetter + "*') AND "
703                            + "(" + NameLookupColumns.NAME_TYPE + " IN("
704                                    + NameLookupType.NAME_COLLATION_KEY + ","
705                                    + NameLookupType.EMAIL_BASED_NICKNAME + ","
706                                    + NameLookupType.NICKNAME + ")) AND "
707                            + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
708                    matchAllCandidates(db, selection, candidates, matcher,
709                            RawContactMatcher.MATCHING_ALGORITHM_APPROXIMATE,
710                            String.valueOf(FIRST_LETTER_SUGGESTION_HIT_LIMIT));
711                }
712            }
713        }
714    }
715
716    private interface ContactNameLookupQuery {
717        String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
718
719        String[] COLUMNS = new String[] {
720                RawContacts._ID,
721                RawContacts.CONTACT_ID,
722                RawContactsColumns.ACCOUNT_ID,
723                NameLookupColumns.NORMALIZED_NAME,
724                NameLookupColumns.NAME_TYPE
725        };
726
727        int RAW_CONTACT_ID = 0;
728        int CONTACT_ID = 1;
729        int ACCOUNT_ID = 2;
730        int NORMALIZED_NAME = 3;
731        int NAME_TYPE = 4;
732    }
733
734    /**
735     * Loads all candidate rows from the name lookup table and updates match scores based
736     * on that data.
737     */
738    private void matchAllCandidates(SQLiteDatabase db, String selection,
739            MatchCandidateList candidates, RawContactMatcher matcher, int algorithm, String limit) {
740        final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
741                selection, null, null, null, null, limit);
742
743        try {
744            while (c.moveToNext()) {
745                Long rawContactId = c.getLong(ContactNameLookupQuery.RAW_CONTACT_ID);
746                Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
747                Long accountId = c.getLong(ContactNameLookupQuery.ACCOUNT_ID);
748                String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
749                int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
750
751                // Note the N^2 complexity of the following fragment. This is not a huge concern
752                // since the number of candidates is very small and in general secondary hits
753                // in the absence of primary hits are rare.
754                for (int i = 0; i < candidates.mCount; i++) {
755                    NameMatchCandidate candidate = candidates.mList.get(i);
756                    matcher.matchName(rawContactId, contactId, accountId, candidate.mLookupType,
757                            candidate.mName, nameType, name, algorithm);
758                }
759            }
760        } finally {
761            c.close();
762        }
763    }
764
765    private interface PhotoFileQuery {
766        final String[] COLUMNS = new String[] {
767                PhotoFiles.HEIGHT,
768                PhotoFiles.WIDTH,
769                PhotoFiles.FILESIZE
770        };
771
772        int HEIGHT = 0;
773        int WIDTH = 1;
774        int FILESIZE = 2;
775    }
776
777    private class PhotoEntry implements Comparable<PhotoEntry> {
778        // Pixel count (width * height) for the image.
779        final int pixelCount;
780
781        // File size (in bytes) of the image.  Not populated if the image is a thumbnail.
782        final int fileSize;
783
784        private PhotoEntry(int pixelCount, int fileSize) {
785            this.pixelCount = pixelCount;
786            this.fileSize = fileSize;
787        }
788
789        @Override
790        public int compareTo(PhotoEntry pe) {
791            if (pe == null) {
792                return -1;
793            }
794            if (pixelCount == pe.pixelCount) {
795                return pe.fileSize - fileSize;
796            } else {
797                return pe.pixelCount - pixelCount;
798            }
799        }
800    }
801
802    /**
803     * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
804     * descending order of match score.
805     * @param parameters
806     */
807    protected List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId,
808            ArrayList<AggregationSuggestionParameter> parameters) {
809
810        MatchCandidateList candidates = new MatchCandidateList();
811        RawContactMatcher matcher = new RawContactMatcher();
812
813        if (parameters == null || parameters.size() == 0) {
814            final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
815                    RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
816            try {
817                while (c.moveToNext()) {
818                    long rawContactId = c.getLong(RawContactIdQuery.RAW_CONTACT_ID);
819                    long accountId = c.getLong(RawContactIdQuery.ACCOUNT_ID);
820                    // Don't aggregate a contact with its own raw contacts.
821                    matcher.keepOut(rawContactId, contactId, accountId);
822                    updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
823                            matcher);
824                }
825            } finally {
826                c.close();
827            }
828        } else {
829            updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates,
830                    matcher, parameters);
831        }
832
833        return matcher.pickBestMatches(SCORE_THRESHOLD_SUGGEST);
834    }
835
836    /**
837     * Computes suggestion scores for contacts that have matching data rows.
838     * Aggregation suggestion doesn't consider aggregation exceptions, but is purely based on the
839     * raw contacts information.
840     */
841    private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
842            long rawContactId, MatchCandidateList candidates, RawContactMatcher matcher) {
843
844        updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
845        updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
846        updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
847        updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
848        loadNameMatchCandidates(db, rawContactId, candidates, false);
849        lookupApproximateNameMatches(db, candidates, matcher);
850    }
851
852    /**
853     * Computes scores for contacts that have matching data rows.
854     */
855    private void updateMatchScores(SQLiteDatabase db, long rawContactId,
856            MatchCandidateList candidates, RawContactMatcher matcher) {
857        //update primary score
858        updateMatchScoresBasedOnExceptions(db, rawContactId, matcher);
859        updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
860        // update scores only if the raw contact doesn't have structured name
861        if (rawContactWithoutName(db, rawContactId)) {
862            updateMatchScoresBasedOnIdentityMatch(db, rawContactId, matcher);
863            updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
864            updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
865            final List<Long> secondaryRawContactIds = matcher.prepareSecondaryMatchCandidates();
866            if (secondaryRawContactIds != null
867                    && secondaryRawContactIds.size() <= SECONDARY_HIT_LIMIT) {
868                updateScoreForCandidatesWithoutName(db, secondaryRawContactIds, matcher);
869            }
870        }
871    }
872
873    private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
874            MatchCandidateList candidates, RawContactMatcher matcher,
875            ArrayList<AggregationSuggestionParameter> parameters) {
876        for (AggregationSuggestionParameter parameter : parameters) {
877            if (AggregationSuggestions.PARAMETER_MATCH_NAME.equals(parameter.kind)) {
878                updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher);
879            }
880
881            // TODO: add support for other parameter kinds
882        }
883    }
884
885    private boolean rawContactWithoutName(SQLiteDatabase db, long rawContactId) {
886        String selection = RawContacts._ID + " =" + rawContactId;
887        final Cursor c = db.query(NullNameRawContactsIdsQuery.TABLE,
888                NullNameRawContactsIdsQuery.COLUMNS, selection, null, null, null, null);
889
890        try {
891            if (c.moveToFirst()) {
892                return TextUtils.isEmpty(c.getString(NullNameRawContactsIdsQuery.NAME));
893            }
894        } finally {
895            c.close();
896        }
897        return false;
898    }
899
900    /**
901     * Update scores for matches with secondary data matching but no structured name.
902     */
903    private void updateScoreForCandidatesWithoutName(SQLiteDatabase db,
904            List<Long> secondaryRawContactIds, RawContactMatcher matcher) {
905
906        mSb.setLength(0);
907
908        mSb.append(RawContacts._ID).append(" IN (");
909        for (int i = 0; i < secondaryRawContactIds.size(); i++) {
910            if (i != 0) {
911                mSb.append(",");
912            }
913            mSb.append(secondaryRawContactIds.get(i));
914        }
915        mSb.append( ")");
916        final Cursor c = db.query(NullNameRawContactsIdsQuery.TABLE,
917                NullNameRawContactsIdsQuery.COLUMNS, mSb.toString(), null, null, null, null);
918
919        try {
920            while (c.moveToNext()) {
921                Long rId = c.getLong(NullNameRawContactsIdsQuery.RAW_CONTACT_ID);
922                Long contactId = c.getLong(NullNameRawContactsIdsQuery.CONTACT_ID);
923                Long accountId = c.getLong(NullNameRawContactsIdsQuery.ACCOUNT_ID);
924                String name = c.getString(NullNameRawContactsIdsQuery.NAME);
925                if (TextUtils.isEmpty(name)) {
926                    matcher.matchNoName(rId, contactId, accountId);
927                }
928            }
929        } finally {
930            c.close();
931        }
932    }
933
934    protected interface IdentityLookupMatchQuery {
935        final String TABLE = Tables.DATA + " dataA"
936                + " JOIN " + Tables.DATA + " dataB" +
937                " ON (dataA." + Identity.NAMESPACE + "=dataB." + Identity.NAMESPACE +
938                " AND dataA." + Identity.IDENTITY + "=dataB." + Identity.IDENTITY + ")"
939                + " JOIN " + Tables.RAW_CONTACTS +
940                " ON (dataB." + Data.RAW_CONTACT_ID + " = "
941                + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
942
943        final String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
944                + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
945                + " AND dataA." + Identity.NAMESPACE + " NOT NULL"
946                + " AND dataA." + Identity.IDENTITY + " NOT NULL"
947                + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
948                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
949
950        final String[] COLUMNS = new String[] {
951                RawContactsColumns.CONCRETE_ID, RawContacts.CONTACT_ID,
952                RawContactsColumns.ACCOUNT_ID
953        };
954
955        int RAW_CONTACT_ID = 0;
956        int CONTACT_ID = 1;
957        int ACCOUNT_ID = 2;
958    }
959
960    protected interface NameLookupMatchQuery {
961        String TABLE = Tables.NAME_LOOKUP + " nameA"
962                + " JOIN " + Tables.NAME_LOOKUP + " nameB" +
963                " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "="
964                + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")"
965                + " JOIN " + Tables.RAW_CONTACTS +
966                " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = "
967                + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
968
969        String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?"
970                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
971
972        String[] COLUMNS = new String[] {
973                RawContacts._ID,
974                RawContacts.CONTACT_ID,
975                RawContactsColumns.ACCOUNT_ID,
976                "nameA." + NameLookupColumns.NORMALIZED_NAME,
977                "nameA." + NameLookupColumns.NAME_TYPE,
978                "nameB." + NameLookupColumns.NAME_TYPE,
979        };
980
981        int RAW_CONTACT_ID = 0;
982        int CONTACT_ID = 1;
983        int ACCOUNT_ID = 2;
984        int NAME = 3;
985        int NAME_TYPE_A = 4;
986        int NAME_TYPE_B = 5;
987    }
988
989    protected interface EmailLookupQuery {
990        String TABLE = Tables.DATA + " dataA"
991                + " JOIN " + Tables.DATA + " dataB" +
992                " ON dataA." + Email.DATA + "= dataB." + Email.DATA
993                + " JOIN " + Tables.RAW_CONTACTS +
994                " ON (dataB." + Data.RAW_CONTACT_ID + " = "
995                + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
996
997        String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?1"
998                + " AND dataA." + DataColumns.MIMETYPE_ID + "=?2"
999                + " AND dataA." + Email.DATA + " NOT NULL"
1000                + " AND dataB." + DataColumns.MIMETYPE_ID + "=?2"
1001                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1002
1003        String[] COLUMNS = new String[] {
1004                Tables.RAW_CONTACTS + "." + RawContacts._ID,
1005                RawContacts.CONTACT_ID,
1006                RawContactsColumns.ACCOUNT_ID
1007        };
1008
1009        int RAW_CONTACT_ID = 0;
1010        int CONTACT_ID = 1;
1011        int ACCOUNT_ID = 2;
1012    }
1013
1014    protected interface PhoneLookupQuery {
1015        String TABLE = Tables.PHONE_LOOKUP + " phoneA"
1016                + " JOIN " + Tables.DATA + " dataA"
1017                + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
1018                + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
1019                + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
1020                + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
1021                + " JOIN " + Tables.DATA + " dataB"
1022                + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
1023                + " JOIN " + Tables.RAW_CONTACTS
1024                + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
1025                + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
1026
1027        String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
1028                + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
1029                + "dataB." + Phone.NUMBER + ",?)"
1030                + " AND " + RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
1031
1032        String[] COLUMNS = new String[] {
1033                Tables.RAW_CONTACTS + "." + RawContacts._ID,
1034                RawContacts.CONTACT_ID,
1035                RawContactsColumns.ACCOUNT_ID
1036        };
1037
1038        int RAW_CONTACT_ID = 0;
1039        int CONTACT_ID = 1;
1040        int ACCOUNT_ID = 2;
1041    }
1042
1043    protected interface NullNameRawContactsIdsQuery {
1044        final String TABLE =  Tables.RAW_CONTACTS + " LEFT OUTER JOIN " +  Tables.NAME_LOOKUP
1045                + " ON "+ RawContacts._ID + " = " + NameLookupColumns.RAW_CONTACT_ID
1046                + " AND " + NameLookupColumns.NAME_TYPE + " = " + NameLookupType.NAME_EXACT;
1047
1048        final String[] COLUMNS = new String[] {
1049                RawContacts._ID, RawContacts.CONTACT_ID, RawContactsColumns.ACCOUNT_ID,
1050                NameLookupColumns.NORMALIZED_NAME};
1051
1052        int RAW_CONTACT_ID = 0;
1053        int CONTACT_ID = 1;
1054        int ACCOUNT_ID = 2;
1055        int NAME = 3;
1056    }
1057}
1058