diff options
Diffstat (limited to 'java/com/android/dialer/searchfragment/cp2')
4 files changed, 309 insertions, 79 deletions
diff --git a/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java b/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java index 6fd053cae..9a0ca0088 100644 --- a/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java +++ b/java/com/android/dialer/searchfragment/cp2/ContactFilterCursor.java @@ -21,17 +21,25 @@ import android.database.CharArrayBuffer; import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; +import android.database.MatrixCursor; import android.net.Uri; import android.os.Bundle; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; import android.support.annotation.IntDef; import android.support.annotation.Nullable; +import android.support.v4.util.ArraySet; import android.text.TextUtils; +import com.android.dialer.common.Assert; import com.android.dialer.searchfragment.common.Projections; import com.android.dialer.searchfragment.common.QueryFilteringUtil; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Set; /** * Wrapper for a cursor containing all on device contacts. @@ -63,92 +71,109 @@ final class ContactFilterCursor implements Cursor { } /** - * @param cursor with projection {@link Projections#PHONE_PROJECTION}. + * @param cursor with projection {@link Projections#DATA_PROJECTION}. * @param query to filter cursor results. */ ContactFilterCursor(Cursor cursor, @Nullable String query) { - // TODO(calderwoodra) investigate copying this into a MatrixCursor and holding in memory - this.cursor = cursor; + this.cursor = createCursor(cursor); filter(query); } /** - * Filters out contacts that do not match the query. + * Returns a new cursor with contact information coalesced. * - * <p>The query can have at least 1 of 3 forms: + * <p>Here are some sample rows and columns that might exist in cp2 database: * * <ul> - * <li>A phone number - * <li>A T9 representation of a name (matches {@link QueryFilteringUtil#T9_PATTERN}). - * <li>A name + * <li>display Name (William), contactID (202), mimeType (name), data1 (A Pixel) + * <li>display Name (William), contactID (202), mimeType (phone), data1 (+1 650-200-3333) + * <li>display Name (William), contactID (202), mimeType (phone), data1 (+1 540-555-6666) + * <li>display Name (William), contactID (202), mimeType (organization), data1 (Walmart) + * <li>display Name (William), contactID (202), mimeType (nickname), data1 (Will) * </ul> * - * <p>A contact is considered a match if: + * <p>These rows would be coalesced into new rows like so: * * <ul> - * <li>Its phone number contains the phone number query - * <li>Its name represented in T9 contains the T9 query - * <li>Its name contains the query + * <li>display Name (William), phoneNumber (+1 650-200-3333), organization (Walmart), nickname + * (Will) + * <li>display Name (William), phoneNumber (+1 540-555-6666), organization (Walmart), nickname + * (Will) * </ul> */ - public void filter(@Nullable String query) { - if (query == null) { - query = ""; + private static Cursor createCursor(Cursor cursor) { + // Convert cursor rows into Cp2Contacts + List<Cp2Contact> cp2Contacts = new ArrayList<>(); + Set<Integer> contactIds = new ArraySet<>(); + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + Cp2Contact contact = Cp2Contact.fromCursor(cursor); + cp2Contacts.add(contact); + contactIds.add(contact.contactId()); } - queryFilteredPositions.clear(); + cursor.close(); - // On some devices, contacts have multiple rows with identical phone numbers. These numbers are - // considered duplicates. Since the order might not be guaranteed, we compare all of the numbers - // and hold onto the most qualified one as the one we want to display to the user. - // See #getQualification for details on how qualification is determined. - int previousMostQualifiedPosition = 0; - String previousName = ""; - String previousMostQualifiedNumber = ""; + // Group then combine contact data + List<Cp2Contact> coalescedContacts = new ArrayList<>(); + for (Integer contactId : contactIds) { + List<Cp2Contact> duplicateContacts = getAllContactsWithContactId(contactId, cp2Contacts); + coalescedContacts.addAll(coalesceContacts(duplicateContacts)); + } - query = query.toLowerCase(); - cursor.moveToPosition(-1); + // Sort by display name, then build new cursor from coalesced contacts. + // We sort the contacts so that they are displayed to the user in lexicographic order. + Collections.sort(coalescedContacts, (o1, o2) -> o1.displayName().compareTo(o2.displayName())); + MatrixCursor newCursor = + new MatrixCursor(Projections.DATA_PROJECTION, coalescedContacts.size()); + for (Cp2Contact contact : coalescedContacts) { + newCursor.addRow(contact.toCursorRow()); + } + return newCursor; + } + + private static List<Cp2Contact> coalesceContacts(List<Cp2Contact> contactsWithSameContactId) { + String companyName = null; + String nickName = null; + List<Cp2Contact> phoneContacts = new ArrayList<>(); + for (Cp2Contact contact : contactsWithSameContactId) { + if (contact.mimeType().equals(Phone.CONTENT_ITEM_TYPE)) { + phoneContacts.add(contact); + } else if (contact.mimeType().equals(Organization.CONTENT_ITEM_TYPE)) { + Assert.checkArgument(TextUtils.isEmpty(companyName)); + companyName = contact.companyName(); + } else if (contact.mimeType().equals(Nickname.CONTENT_ITEM_TYPE)) { + Assert.checkArgument(TextUtils.isEmpty(nickName)); + nickName = contact.nickName(); + } + } - while (cursor.moveToNext()) { - int position = cursor.getPosition(); - String currentNumber = cursor.getString(Projections.PHONE_NUMBER); - String currentName = cursor.getString(Projections.PHONE_DISPLAY_NAME); + removeDuplicatePhoneNumbers(phoneContacts); - if (!previousName.equals(currentName)) { - previousName = currentName; - previousMostQualifiedNumber = currentNumber; - previousMostQualifiedPosition = position; - } else { - // Since the contact name is the same, check if this number is a duplicate - switch (getQualification(currentNumber, previousMostQualifiedNumber)) { - case Qualification.CURRENT_MORE_QUALIFIED: - // Number is a less qualified duplicate, ignore it. - continue; - case Qualification.NEW_NUMBER_IS_MORE_QUALIFIED: - // If number wasn't filtered out before, remove it and add it's more qualified version. - int index = queryFilteredPositions.indexOf(previousMostQualifiedPosition); - if (index != -1) { - queryFilteredPositions.remove(index); - queryFilteredPositions.add(position); - } - previousMostQualifiedNumber = currentNumber; - previousMostQualifiedPosition = position; - continue; - case Qualification.NUMBERS_ARE_NOT_DUPLICATES: - default: - previousMostQualifiedNumber = currentNumber; - previousMostQualifiedPosition = position; + List<Cp2Contact> coalescedContacts = new ArrayList<>(); + for (Cp2Contact phoneContact : phoneContacts) { + coalescedContacts.add( + phoneContact.toBuilder().setCompanyName(companyName).setNickName(nickName).build()); + } + return coalescedContacts; + } + + private static void removeDuplicatePhoneNumbers(List<Cp2Contact> phoneContacts) { + for (int i = 0; i < phoneContacts.size(); i++) { + Cp2Contact contact1 = phoneContacts.get(i); + for (int j = i + 1; j < phoneContacts.size(); /* don't iterate by default */ ) { + Cp2Contact contact2 = phoneContacts.get(j); + int qualification = getQualification(contact2.phoneNumber(), contact1.phoneNumber()); + if (qualification == Qualification.CURRENT_MORE_QUALIFIED) { + phoneContacts.remove(contact2); + } else if (qualification == Qualification.NEW_NUMBER_IS_MORE_QUALIFIED) { + phoneContacts.remove(contact1); + break; + } else if (qualification == Qualification.NUMBERS_ARE_NOT_DUPLICATES) { + // Keep both contacts + j++; } } - - if (TextUtils.isEmpty(query) - || QueryFilteringUtil.nameMatchesT9Query(query, previousName) - || QueryFilteringUtil.numberMatchesNumberQuery(query, previousMostQualifiedNumber) - || QueryFilteringUtil.nameContainsQuery(query, previousName)) { - queryFilteredPositions.add(previousMostQualifiedPosition); - } } - currentPosition = 0; - cursor.moveToFirst(); } /** @@ -157,7 +182,7 @@ final class ContactFilterCursor implements Cursor { * @return {@link Qualification} where the more qualified number is the number with the most * digits. If the digits are the same, the number with the most formatting is more qualified. */ - private @Qualification int getQualification(String number, String mostQualifiedNumber) { + private static @Qualification int getQualification(String number, String mostQualifiedNumber) { // Ignore formatting String numberDigits = QueryFilteringUtil.digitsOnly(number); String qualifiedNumberDigits = QueryFilteringUtil.digitsOnly(mostQualifiedNumber); @@ -182,6 +207,64 @@ final class ContactFilterCursor implements Cursor { return Qualification.NUMBERS_ARE_NOT_DUPLICATES; } + private static List<Cp2Contact> getAllContactsWithContactId( + int contactId, List<Cp2Contact> contacts) { + List<Cp2Contact> contactIdContacts = new ArrayList<>(); + for (Cp2Contact contact : contacts) { + if (contact.contactId() == contactId) { + contactIdContacts.add(contact); + } + } + return contactIdContacts; + } + + /** + * Filters out contacts that do not match the query. + * + * <p>The query can have at least 1 of 3 forms: + * + * <ul> + * <li>A phone number + * <li>A T9 representation of a name (matches {@link QueryFilteringUtil#T9_PATTERN}). + * <li>A name + * </ul> + * + * <p>A contact is considered a match if: + * + * <ul> + * <li>Its phone number contains the phone number query + * <li>Its name represented in T9 contains the T9 query + * <li>Its name contains the query + * <li>Its company contains the query + * </ul> + */ + public void filter(@Nullable String query) { + if (query == null) { + query = ""; + } + queryFilteredPositions.clear(); + query = query.toLowerCase(); + cursor.moveToPosition(-1); + + while (cursor.moveToNext()) { + int position = cursor.getPosition(); + String number = cursor.getString(Projections.PHONE_NUMBER); + String name = cursor.getString(Projections.DISPLAY_NAME); + String companyName = cursor.getString(Projections.COMPANY_NAME); + String nickName = cursor.getString(Projections.NICKNAME); + if (TextUtils.isEmpty(query) + || QueryFilteringUtil.nameMatchesT9Query(query, name) + || QueryFilteringUtil.numberMatchesNumberQuery(query, number) + || QueryFilteringUtil.nameContainsQuery(query, name) + || QueryFilteringUtil.nameContainsQuery(query, companyName) + || QueryFilteringUtil.nameContainsQuery(query, nickName)) { + queryFilteredPositions.add(position); + } + } + currentPosition = 0; + cursor.moveToFirst(); + } + @Override public boolean moveToPosition(int position) { currentPosition = position; diff --git a/java/com/android/dialer/searchfragment/cp2/Cp2Contact.java b/java/com/android/dialer/searchfragment/cp2/Cp2Contact.java new file mode 100644 index 000000000..f199f679b --- /dev/null +++ b/java/com/android/dialer/searchfragment/cp2/Cp2Contact.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.searchfragment.cp2; + +import android.database.Cursor; +import android.support.annotation.Nullable; +import com.android.dialer.searchfragment.common.Projections; +import com.google.auto.value.AutoValue; + +/** POJO Representation for contacts returned in {@link SearchContactsCursorLoader}. */ +@AutoValue +public abstract class Cp2Contact { + + public abstract long phoneId(); + + public abstract int phoneType(); + + @Nullable + public abstract String phoneLabel(); + + public abstract String phoneNumber(); + + @Nullable + public abstract String displayName(); + + public abstract int photoId(); + + @Nullable + public abstract String photoUri(); + + public abstract String lookupKey(); + + public abstract int carrierPresence(); + + public abstract int contactId(); + + @Nullable + public abstract String companyName(); + + @Nullable + public abstract String nickName(); + + public abstract String mimeType(); + + /** Builder for {@link Cp2Contact}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setPhoneId(long id); + + public abstract Builder setPhoneType(int type); + + public abstract Builder setPhoneLabel(@Nullable String label); + + public abstract Builder setPhoneNumber(String number); + + public abstract Builder setDisplayName(@Nullable String name); + + public abstract Builder setPhotoId(int id); + + public abstract Builder setPhotoUri(@Nullable String uri); + + public abstract Builder setLookupKey(String lookupKey); + + public abstract Builder setCarrierPresence(int presence); + + public abstract Builder setContactId(int id); + + public abstract Builder setCompanyName(@Nullable String name); + + public abstract Builder setNickName(@Nullable String nickName); + + public abstract Builder setMimeType(String mimeType); + + public abstract Cp2Contact build(); + } + + public static Builder builder() { + return new AutoValue_Cp2Contact.Builder(); + } + + public static Cp2Contact fromCursor(Cursor cursor) { + return Cp2Contact.builder() + .setPhoneId(cursor.getLong(Projections.CONTACT_ID)) + .setPhoneType(cursor.getInt(Projections.PHONE_TYPE)) + .setPhoneLabel(cursor.getString(Projections.PHONE_LABEL)) + .setPhoneNumber(cursor.getString(Projections.PHONE_NUMBER)) + .setDisplayName(cursor.getString(Projections.DISPLAY_NAME)) + .setPhotoId(cursor.getInt(Projections.PHOTO_ID)) + .setPhotoUri(cursor.getString(Projections.PHOTO_URI)) + .setLookupKey(cursor.getString(Projections.LOOKUP_KEY)) + .setCarrierPresence(cursor.getInt(Projections.CARRIER_PRESENCE)) + .setContactId(cursor.getInt(Projections.CONTACT_ID)) + .setCompanyName(cursor.getString(Projections.COMPANY_NAME)) + .setNickName(cursor.getString(Projections.NICKNAME)) + .setMimeType(cursor.getString(Projections.MIME_TYPE)) + .build(); + } + + public Object[] toCursorRow() { + Object[] row = new Object[Projections.DATA_PROJECTION.length]; + row[Projections.ID] = phoneId(); + row[Projections.PHONE_TYPE] = phoneType(); + row[Projections.PHONE_LABEL] = phoneLabel(); + row[Projections.PHONE_NUMBER] = phoneNumber(); + row[Projections.DISPLAY_NAME] = displayName(); + row[Projections.PHOTO_ID] = photoId(); + row[Projections.PHOTO_URI] = photoUri(); + row[Projections.LOOKUP_KEY] = lookupKey(); + row[Projections.CARRIER_PRESENCE] = carrierPresence(); + row[Projections.CONTACT_ID] = contactId(); + row[Projections.COMPANY_NAME] = companyName(); + row[Projections.NICKNAME] = nickName(); + row[Projections.MIME_TYPE] = mimeType(); + return row; + } + + public abstract Builder toBuilder(); +} diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java index b162a5e52..c09396c72 100644 --- a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java +++ b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java @@ -96,7 +96,7 @@ public final class SearchContactViewHolder extends ViewHolder implements OnClick dialerContact = getDialerContact(context, cursor); position = cursor.getPosition(); number = cursor.getString(Projections.PHONE_NUMBER); - String name = cursor.getString(Projections.PHONE_DISPLAY_NAME); + String name = cursor.getString(Projections.DISPLAY_NAME); String label = getLabel(context.getResources(), cursor); String secondaryInfo = TextUtils.isEmpty(label) @@ -111,12 +111,12 @@ public final class SearchContactViewHolder extends ViewHolder implements OnClick if (shouldShowPhoto(cursor)) { nameOrNumberView.setVisibility(View.VISIBLE); photo.setVisibility(View.VISIBLE); - String photoUri = cursor.getString(Projections.PHONE_PHOTO_URI); + String photoUri = cursor.getString(Projections.PHOTO_URI); ContactPhotoManager.getInstance(context) .loadDialerThumbnailOrPhoto( photo, getContactUri(cursor), - cursor.getLong(Projections.PHONE_PHOTO_ID), + cursor.getLong(Projections.PHOTO_ID), photoUri == null ? null : Uri.parse(photoUri), name, LetterTileDrawable.TYPE_DEFAULT); @@ -129,11 +129,11 @@ public final class SearchContactViewHolder extends ViewHolder implements OnClick // Show the contact photo next to only the first number if a contact has multiple numbers private boolean shouldShowPhoto(SearchCursor cursor) { int currentPosition = cursor.getPosition(); - String currentLookupKey = cursor.getString(Projections.PHONE_LOOKUP_KEY); + String currentLookupKey = cursor.getString(Projections.LOOKUP_KEY); cursor.moveToPosition(currentPosition - 1); if (!cursor.isHeader() && !cursor.isBeforeFirst()) { - String previousLookupKey = cursor.getString(Projections.PHONE_LOOKUP_KEY); + String previousLookupKey = cursor.getString(Projections.LOOKUP_KEY); cursor.moveToPosition(currentPosition); return !currentLookupKey.equals(previousLookupKey); } @@ -142,8 +142,8 @@ public final class SearchContactViewHolder extends ViewHolder implements OnClick } private static Uri getContactUri(Cursor cursor) { - long contactId = cursor.getLong(Projections.PHONE_ID); - String lookupKey = cursor.getString(Projections.PHONE_LOOKUP_KEY); + long contactId = cursor.getLong(Projections.ID); + String lookupKey = cursor.getString(Projections.LOOKUP_KEY); return Contacts.getLookupUri(contactId, lookupKey); } @@ -188,7 +188,7 @@ public final class SearchContactViewHolder extends ViewHolder implements OnClick private static @CallToAction int getCallToAction( Context context, SearchCursor cursor, String query) { - int carrierPresence = cursor.getInt(Projections.PHONE_CARRIER_PRESENCE); + int carrierPresence = cursor.getInt(Projections.CARRIER_PRESENCE); String number = cursor.getString(Projections.PHONE_NUMBER); if ((carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) == 1) { return CallToAction.VIDEO_CALL; @@ -262,16 +262,15 @@ public final class SearchContactViewHolder extends ViewHolder implements OnClick private static DialerContact getDialerContact(Context context, Cursor cursor) { DialerContact.Builder contact = DialerContact.newBuilder(); - String displayName = cursor.getString(Projections.PHONE_DISPLAY_NAME); + String displayName = cursor.getString(Projections.DISPLAY_NAME); String number = cursor.getString(Projections.PHONE_NUMBER); Uri contactUri = Contacts.getLookupUri( - cursor.getLong(Projections.PHONE_CONTACT_ID), - cursor.getString(Projections.PHONE_LOOKUP_KEY)); + cursor.getLong(Projections.CONTACT_ID), cursor.getString(Projections.LOOKUP_KEY)); contact .setNumber(number) - .setPhotoId(cursor.getLong(Projections.PHONE_PHOTO_ID)) + .setPhotoId(cursor.getLong(Projections.PHOTO_ID)) .setContactType(LetterTileDrawable.TYPE_DEFAULT) .setNameOrNumber(displayName) .setNumberLabel( @@ -281,7 +280,7 @@ public final class SearchContactViewHolder extends ViewHolder implements OnClick cursor.getString(Projections.PHONE_LABEL)) .toString()); - String photoUri = cursor.getString(Projections.PHONE_PHOTO_URI); + String photoUri = cursor.getString(Projections.PHOTO_URI); if (photoUri != null) { contact.setPhotoUri(photoUri); } diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java index b7fc9b5c5..f1230c6d9 100644 --- a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java +++ b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java @@ -19,7 +19,10 @@ package com.android.dialer.searchfragment.cp2; import android.content.Context; import android.content.CursorLoader; import android.database.Cursor; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Data; import android.support.annotation.Nullable; import com.android.dialer.searchfragment.common.Projections; @@ -32,14 +35,27 @@ public final class SearchContactsCursorLoader extends CursorLoader { public SearchContactsCursorLoader(Context context, @Nullable String query) { super( context, - Phone.CONTENT_URI, - Projections.PHONE_PROJECTION, - null, + Data.CONTENT_URI, + Projections.DATA_PROJECTION, + whereStatement(), null, Phone.SORT_KEY_PRIMARY + " ASC"); this.query = query; } + private static String whereStatement() { + return (Phone.NUMBER + " IS NOT NULL") + + " AND " + + Data.MIMETYPE + + " IN (\'" + + Phone.CONTENT_ITEM_TYPE + + "\', \'" + + Nickname.CONTENT_ITEM_TYPE + + "\', \'" + + Organization.CONTENT_ITEM_TYPE + + "\')"; + } + @Override public Cursor loadInBackground() { // All contacts |