From 6af2e14a28e27a38525a08c920e7453c2448689a Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Fri, 3 Nov 2017 15:53:04 -0700 Subject: Implement bulk update for Cp2PhoneLookup. Test: Cp2PhoneLookupTest PiperOrigin-RevId: 174525877 Change-Id: I7888f3b6adc58416c560271166ec6bd85306d58b --- .../dialer/phonelookup/cp2/Cp2PhoneLookup.java | 337 ++++++++++++++++++--- .../dialer/phonelookup/phone_lookup_info.proto | 17 +- 2 files changed, 318 insertions(+), 36 deletions(-) (limited to 'java/com/android/dialer/phonelookup') diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java index f9fc1a6f4..2878e27c4 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java @@ -22,23 +22,47 @@ import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.DeletedContacts; import android.support.annotation.NonNull; +import android.support.v4.util.ArrayMap; import android.support.v4.util.ArraySet; import android.telecom.Call; +import android.text.TextUtils; import com.android.dialer.DialerPhoneNumber; +import com.android.dialer.common.Assert; import com.android.dialer.common.concurrent.DialerExecutors; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; +import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import javax.inject.Inject; /** PhoneLookup implementation for local contacts. */ public final class Cp2PhoneLookup implements PhoneLookup { + private static final String[] CP2_INFO_PROJECTION = + new String[] { + Phone.DISPLAY_NAME_PRIMARY, // 0 + Phone.PHOTO_THUMBNAIL_URI, // 1 + Phone.PHOTO_ID, // 2 + Phone.LABEL, // 3 + Phone.NORMALIZED_NUMBER, // 4 + Phone.CONTACT_ID, // 5 + }; + + private static final int CP2_INFO_NAME_INDEX = 0; + private static final int CP2_INFO_PHOTO_URI_INDEX = 1; + private static final int CP2_INFO_PHOTO_ID_INDEX = 2; + private static final int CP2_INFO_LABEL_INDEX = 3; + private static final int CP2_INFO_NUMBER_INDEX = 4; + private static final int CP2_INFO_CONTACT_ID_INDEX = 5; + private final Context appContext; @Inject @@ -60,12 +84,12 @@ public final class Cp2PhoneLookup implements PhoneLookup { } private boolean isDirtyInternal(ImmutableSet phoneNumbers, long lastModified) { - return contactsUpdated(getContactIdsFromPhoneNumbers(phoneNumbers), lastModified) + return contactsUpdated(queryPhoneTableForContactIds(phoneNumbers), lastModified) || contactsDeleted(lastModified); } /** Returns set of contact ids that correspond to {@code phoneNumbers} if the contact exists. */ - private Set getContactIdsFromPhoneNumbers(ImmutableSet phoneNumbers) { + private Set queryPhoneTableForContactIds(ImmutableSet phoneNumbers) { Set contactIds = new ArraySet<>(); try (Cursor cursor = appContext @@ -73,7 +97,7 @@ public final class Cp2PhoneLookup implements PhoneLookup { .query( Phone.CONTENT_URI, new String[] {Phone.CONTACT_ID}, - columnInSetWhereStatement(Phone.NORMALIZED_NUMBER, phoneNumbers.size()), + Phone.NORMALIZED_NUMBER + " IN (" + questionMarks(phoneNumbers.size()) + ")", contactIdsSelectionArgs(phoneNumbers), null)) { cursor.moveToPosition(-1); @@ -100,37 +124,32 @@ public final class Cp2PhoneLookup implements PhoneLookup { /** Returns true if any contacts were modified after {@code lastModified}. */ private boolean contactsUpdated(Set contactIds, long lastModified) { - try (Cursor cursor = - appContext - .getContentResolver() - .query( - Contacts.CONTENT_URI, - new String[] {Contacts._ID}, - contactsIsDirtyWhereStatement(contactIds.size()), - contactsIsDirtySelectionArgs(lastModified, contactIds), - null)) { + try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) { return cursor.getCount() > 0; } } - private static String contactsIsDirtyWhereStatement(int numberOfContactIds) { - StringBuilder where = new StringBuilder(); - // Filter to after last modified time - where.append(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP).append(" > ?"); - - // Filter based only on contacts we care about - where.append(" AND ").append(columnInSetWhereStatement(Contacts._ID, numberOfContactIds)); - return where.toString(); - } + private Cursor queryContactsTableForContacts(Set contactIds, long lastModified) { + // Filter to after last modified time based only on contacts we care about + String where = + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + + " > ?" + + " AND " + + Contacts._ID + + " IN (" + + questionMarks(contactIds.size()) + + ")"; - private String[] contactsIsDirtySelectionArgs(long lastModified, Set contactIds) { String[] args = new String[contactIds.size() + 1]; args[0] = Long.toString(lastModified); int i = 1; for (Long contactId : contactIds) { args[i++] = Long.toString(contactId); } - return args; + + return appContext + .getContentResolver() + .query(Contacts.CONTENT_URI, new String[] {Contacts._ID}, where, args, null); } /** Returns true if any contacts were deleted after {@code lastModified}. */ @@ -148,22 +167,272 @@ public final class Cp2PhoneLookup implements PhoneLookup { } } - private static String columnInSetWhereStatement(String columnName, int setSize) { + @Override + public ListenableFuture> bulkUpdate( + ImmutableMap existingInfoMap, long lastModified) { + return MoreExecutors.listeningDecorator(DialerExecutors.getLowPriorityThreadPool(appContext)) + .submit(() -> bulkUpdateInternal(existingInfoMap, lastModified)); + } + + private ImmutableMap bulkUpdateInternal( + ImmutableMap existingInfoMap, long lastModified) { + // Build a set of each DialerPhoneNumber that was associated with a contact, and is no longer + // associated with that same contact. + Set deletedPhoneNumbers = + getDeletedPhoneNumbers(existingInfoMap, lastModified); + + // For each DialerPhoneNumber that was associated with a contact or added to a contact, + // build a map of those DialerPhoneNumbers to a set Cp2Infos, where each Cp2Info represents a + // contact. + ImmutableMap> updatedContacts = + buildMapForUpdatedOrAddedContacts(existingInfoMap, lastModified, deletedPhoneNumbers); + + // Start build a new map of updated info. This will replace existing info. + ImmutableMap.Builder newInfoMapBuilder = + ImmutableMap.builder(); + + // For each DialerPhoneNumber in existing info... + for (Entry entry : existingInfoMap.entrySet()) { + // Build off the existing info + PhoneLookupInfo.Builder infoBuilder = PhoneLookupInfo.newBuilder(entry.getValue()); + + // If the contact was updated, replace the Cp2Info list + if (updatedContacts.containsKey(entry.getKey())) { + infoBuilder.clearCp2Info(); + infoBuilder.addAllCp2Info(updatedContacts.get(entry.getKey())); + + // If it was deleted and not added to a new contact, replace the Cp2Info list with + // the default instance of Cp2Info + } else if (deletedPhoneNumbers.contains(entry.getKey())) { + infoBuilder.clearCp2Info(); + infoBuilder.addCp2Info(Cp2Info.getDefaultInstance()); + } + + // If the DialerPhoneNumber didn't change, add the unchanged existing info. + newInfoMapBuilder.put(entry.getKey(), infoBuilder.build()); + } + return newInfoMapBuilder.build(); + } + + /** + * 1. get all contact ids. if the id is unset, add the number to the list of contacts to look up. + * 2. reduce our list of contact ids to those that were updated after lastModified. 3. Now we have + * the smallest set of dialer phone numbers to query cp2 against. 4. build and return the map of + * dialerphonenumbers to their new cp2info + * + * @return Map of {@link DialerPhoneNumber} to {@link PhoneLookupInfo} with updated {@link + * Cp2Info}. + */ + private ImmutableMap> buildMapForUpdatedOrAddedContacts( + ImmutableMap existingInfoMap, + long lastModified, + Set deletedPhoneNumbers) { + + // Start building a set of DialerPhoneNumbers that we want to update. + Set updatedNumbers = new ArraySet<>(); + + Set contactIds = new ArraySet<>(); + for (Entry entry : existingInfoMap.entrySet()) { + // If the number was deleted, we need to check if it was added to a new contact. + if (deletedPhoneNumbers.contains(entry.getKey())) { + updatedNumbers.add(entry.getKey()); + continue; + } + + // For each Cp2Info for each existing DialerPhoneNumber... + // Store the contact id if it exist, else automatically add the DialerPhoneNumber to our + // set of DialerPhoneNumbers we want to update. + for (Cp2Info cp2Info : entry.getValue().getCp2InfoList()) { + if (Objects.equals(cp2Info, Cp2Info.getDefaultInstance())) { + // If the number doesn't have any Cp2Info set to it, for various reasons, we need to look + // up the number to check if any exists. + // The various reasons this might happen are: + // - An existing contact that wasn't in the call log is now in the call log. + // - A number was in the call log before but has now been added to a contact. + // - A number is in the call log, but isn't associated with any contact. + updatedNumbers.add(entry.getKey()); + } else { + contactIds.add(cp2Info.getContactId()); + } + } + } + + // Query the contacts table and get those that whose Contacts.CONTACT_LAST_UPDATED_TIMESTAMP is + // after lastModified, such that Contacts._ID is in our set of contact IDs we build above. + try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) { + int contactIdIndex = cursor.getColumnIndex(Contacts._ID); + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + // Find the DialerPhoneNumber for each contact id and add it to our updated numbers set. + // These, along with our number not associated with any Cp2Info need to be updated. + long contactId = cursor.getLong(contactIdIndex); + updatedNumbers.addAll(getDialerPhoneNumber(existingInfoMap, contactId)); + } + } + + // Query the Phone table and build Cp2Info for each DialerPhoneNumber in our updatedNumbers set. + Map> map = new ArrayMap<>(); + try (Cursor cursor = getAllCp2Rows(updatedNumbers)) { + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + // Map each dialer phone number to it's new cp2 info + Set phoneNumbers = + getDialerPhoneNumbers(updatedNumbers, cursor.getString(CP2_INFO_NUMBER_INDEX)); + Cp2Info info = buildCp2InfoFromUpdatedContactsCursor(cursor); + for (DialerPhoneNumber phoneNumber : phoneNumbers) { + if (map.containsKey(phoneNumber)) { + map.get(phoneNumber).add(info); + } else { + Set cp2Infos = new ArraySet<>(); + cp2Infos.add(info); + map.put(phoneNumber, cp2Infos); + } + } + } + } + return ImmutableMap.copyOf(map); + } + + /** + * Returns cursor with projection {@link #CP2_INFO_PROJECTION} and only phone numbers that are in + * {@code updateNumbers}. + */ + private Cursor getAllCp2Rows(Set updatedNumbers) { + String where = Phone.NORMALIZED_NUMBER + " IN (" + questionMarks(updatedNumbers.size()) + ")"; + String[] selectionArgs = new String[updatedNumbers.size()]; + int i = 0; + for (DialerPhoneNumber phoneNumber : updatedNumbers) { + selectionArgs[i++] = getNormalizedNumber(phoneNumber); + } + + return appContext + .getContentResolver() + .query(Phone.CONTENT_URI, CP2_INFO_PROJECTION, where, selectionArgs, null); + } + + /** + * @param cursor with projection {@link #CP2_INFO_PROJECTION}. + * @return new {@link Cp2Info} based on current row of {@code cursor}. + */ + private static Cp2Info buildCp2InfoFromUpdatedContactsCursor(Cursor cursor) { + String displayName = cursor.getString(CP2_INFO_NAME_INDEX); + String photoUri = cursor.getString(CP2_INFO_PHOTO_URI_INDEX); + String label = cursor.getString(CP2_INFO_LABEL_INDEX); + + Cp2Info.Builder infoBuilder = Cp2Info.newBuilder(); + if (!TextUtils.isEmpty(displayName)) { + infoBuilder.setName(displayName); + } + if (!TextUtils.isEmpty(photoUri)) { + infoBuilder.setPhotoUri(photoUri); + } + if (!TextUtils.isEmpty(label)) { + infoBuilder.setLabel(label); + } + infoBuilder.setPhotoId(cursor.getLong(CP2_INFO_PHOTO_ID_INDEX)); + infoBuilder.setContactId(cursor.getLong(CP2_INFO_CONTACT_ID_INDEX)); + return infoBuilder.build(); + } + + /** Returns set of DialerPhoneNumbers that were associated with now deleted contacts. */ + private Set getDeletedPhoneNumbers( + ImmutableMap existingInfoMap, long lastModified) { + // Build set of all contact IDs from our existing data. We're going to use this set to query + // against the DeletedContacts table and see if any of them were deleted. + Set contactIds = findContactIdsIn(existingInfoMap); + + // Start building a set of DialerPhoneNumbers that were associated with now deleted contacts. + try (Cursor cursor = queryDeletedContacts(contactIds, lastModified)) { + // We now have a cursor/list of contact IDs that were associated with deleted contacts. + return findDeletedPhoneNumbersIn(existingInfoMap, cursor); + } + } + + private Set findContactIdsIn(ImmutableMap map) { + Set contactIds = new ArraySet<>(); + for (PhoneLookupInfo info : map.values()) { + for (Cp2Info cp2Info : info.getCp2InfoList()) { + contactIds.add(cp2Info.getContactId()); + } + } + return contactIds; + } + + private Cursor queryDeletedContacts(Set contactIds, long lastModified) { + String where = + DeletedContacts.CONTACT_DELETED_TIMESTAMP + + " > ?" + + " AND " + + DeletedContacts.CONTACT_ID + + " IN (" + + questionMarks(contactIds.size()) + + ")"; + String[] args = new String[contactIds.size() + 1]; + args[0] = Long.toString(lastModified); + int i = 1; + for (Long contactId : contactIds) { + args[i++] = Long.toString(contactId); + } + + return appContext + .getContentResolver() + .query( + DeletedContacts.CONTENT_URI, + new String[] {DeletedContacts.CONTACT_ID}, + where, + args, + null); + } + + /** Returns set of DialerPhoneNumbers that are associated with deleted contact IDs. */ + private Set findDeletedPhoneNumbersIn( + ImmutableMap existingInfoMap, Cursor cursor) { + int contactIdIndex = cursor.getColumnIndexOrThrow(DeletedContacts.CONTACT_ID); + Set deletedPhoneNumbers = new ArraySet<>(); + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + long contactId = cursor.getLong(contactIdIndex); + deletedPhoneNumbers.addAll(getDialerPhoneNumber(existingInfoMap, contactId)); + } + return deletedPhoneNumbers; + } + + private static Set getDialerPhoneNumbers( + Set phoneNumbers, String number) { + Set matches = new ArraySet<>(); + for (DialerPhoneNumber phoneNumber : phoneNumbers) { + if (getNormalizedNumber(phoneNumber).equals(number)) { + matches.add(phoneNumber); + } + } + Assert.checkArgument( + matches.size() > 0, "Couldn't find DialerPhoneNumber for number: " + number); + return matches; + } + + private static Set getDialerPhoneNumber( + ImmutableMap existingInfoMap, long contactId) { + Set matches = new ArraySet<>(); + for (Entry entry : existingInfoMap.entrySet()) { + for (Cp2Info cp2Info : entry.getValue().getCp2InfoList()) { + if (cp2Info.getContactId() == contactId) { + matches.add(entry.getKey()); + } + } + } + Assert.checkArgument( + matches.size() > 0, "Couldn't find DialerPhoneNumber for contact ID: " + contactId); + return matches; + } + + private static String questionMarks(int count) { StringBuilder where = new StringBuilder(); - where.append(columnName).append(" IN ("); - for (int i = 0; i < setSize; i++) { + for (int i = 0; i < count; i++) { if (i != 0) { where.append(", "); } where.append("?"); } - return where.append(")").toString(); - } - - @Override - public ListenableFuture> bulkUpdate( - ImmutableMap existingInfoMap, long lastModified) { - // TODO(calderwoodra) - return null; + return where.toString(); } } diff --git a/java/com/android/dialer/phonelookup/phone_lookup_info.proto b/java/com/android/dialer/phonelookup/phone_lookup_info.proto index 1027e5c22..cb89a64e3 100644 --- a/java/com/android/dialer/phonelookup/phone_lookup_info.proto +++ b/java/com/android/dialer/phonelookup/phone_lookup_info.proto @@ -17,10 +17,23 @@ message PhoneLookupInfo { // Information about a PhoneNumber retrieved from CP2. Cp2PhoneLookup is // responsible for populating the data in this message. message Cp2Info { + // android.provider.ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_PRIMARY optional string name = 1; + + // android.provider.ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI optional string photo_uri = 2; + + // android.provider.ContactsContract.CommonDataKinds.Phone.PHOTO_ID optional fixed64 photo_id = 3; - optional string label = 4; // "Home", "Mobile", ect. + + // android.provider.ContactsContract.CommonDataKinds.Phone.LABEL + // "Home", "Mobile", ect. + optional string label = 4; + + // android.provider.ContactsContract.CommonDataKinds.Phone.CONTACT_ID + optional fixed64 contact_id = 5; } - optional Cp2Info cp2_info = 1; + // Repeated because one phone number can be associated with multiple CP2 + // contacts. + repeated Cp2Info cp2_info = 1; } \ No newline at end of file -- cgit v1.2.3