summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/phonelookup
diff options
context:
space:
mode:
authorcalderwoodra <calderwoodra@google.com>2017-11-03 15:53:04 -0700
committerzachh <zachh@google.com>2017-11-11 06:38:50 +0000
commit6af2e14a28e27a38525a08c920e7453c2448689a (patch)
tree1f767a012ce5894534f06a68dc5fac73bf0d921f /java/com/android/dialer/phonelookup
parent1fe02f5e572970e21b760d5774483bc859634982 (diff)
Implement bulk update for Cp2PhoneLookup.
Test: Cp2PhoneLookupTest PiperOrigin-RevId: 174525877 Change-Id: I7888f3b6adc58416c560271166ec6bd85306d58b
Diffstat (limited to 'java/com/android/dialer/phonelookup')
-rw-r--r--java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java337
-rw-r--r--java/com/android/dialer/phonelookup/phone_lookup_info.proto17
2 files changed, 318 insertions, 36 deletions
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<DialerPhoneNumber> 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<Long> getContactIdsFromPhoneNumbers(ImmutableSet<DialerPhoneNumber> phoneNumbers) {
+ private Set<Long> queryPhoneTableForContactIds(ImmutableSet<DialerPhoneNumber> phoneNumbers) {
Set<Long> 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<Long> 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<Long> 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<Long> 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<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> bulkUpdate(
+ ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long lastModified) {
+ return MoreExecutors.listeningDecorator(DialerExecutors.getLowPriorityThreadPool(appContext))
+ .submit(() -> bulkUpdateInternal(existingInfoMap, lastModified));
+ }
+
+ private ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> bulkUpdateInternal(
+ ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> 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<DialerPhoneNumber> 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<DialerPhoneNumber, Set<Cp2Info>> updatedContacts =
+ buildMapForUpdatedOrAddedContacts(existingInfoMap, lastModified, deletedPhoneNumbers);
+
+ // Start build a new map of updated info. This will replace existing info.
+ ImmutableMap.Builder<DialerPhoneNumber, PhoneLookupInfo> newInfoMapBuilder =
+ ImmutableMap.builder();
+
+ // For each DialerPhoneNumber in existing info...
+ for (Entry<DialerPhoneNumber, PhoneLookupInfo> 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<DialerPhoneNumber, Set<Cp2Info>> buildMapForUpdatedOrAddedContacts(
+ ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap,
+ long lastModified,
+ Set<DialerPhoneNumber> deletedPhoneNumbers) {
+
+ // Start building a set of DialerPhoneNumbers that we want to update.
+ Set<DialerPhoneNumber> updatedNumbers = new ArraySet<>();
+
+ Set<Long> contactIds = new ArraySet<>();
+ for (Entry<DialerPhoneNumber, PhoneLookupInfo> 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<DialerPhoneNumber, Set<Cp2Info>> 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<DialerPhoneNumber> 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<Cp2Info> 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<DialerPhoneNumber> 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<DialerPhoneNumber> getDeletedPhoneNumbers(
+ ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> 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<Long> 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<Long> findContactIdsIn(ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> map) {
+ Set<Long> contactIds = new ArraySet<>();
+ for (PhoneLookupInfo info : map.values()) {
+ for (Cp2Info cp2Info : info.getCp2InfoList()) {
+ contactIds.add(cp2Info.getContactId());
+ }
+ }
+ return contactIds;
+ }
+
+ private Cursor queryDeletedContacts(Set<Long> 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<DialerPhoneNumber> findDeletedPhoneNumbersIn(
+ ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, Cursor cursor) {
+ int contactIdIndex = cursor.getColumnIndexOrThrow(DeletedContacts.CONTACT_ID);
+ Set<DialerPhoneNumber> deletedPhoneNumbers = new ArraySet<>();
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ long contactId = cursor.getLong(contactIdIndex);
+ deletedPhoneNumbers.addAll(getDialerPhoneNumber(existingInfoMap, contactId));
+ }
+ return deletedPhoneNumbers;
+ }
+
+ private static Set<DialerPhoneNumber> getDialerPhoneNumbers(
+ Set<DialerPhoneNumber> phoneNumbers, String number) {
+ Set<DialerPhoneNumber> 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<DialerPhoneNumber> getDialerPhoneNumber(
+ ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> existingInfoMap, long contactId) {
+ Set<DialerPhoneNumber> matches = new ArraySet<>();
+ for (Entry<DialerPhoneNumber, PhoneLookupInfo> 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<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> bulkUpdate(
- ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> 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