From 99817db9f0369747c22a4e03ad1f1db26c5d81bb Mon Sep 17 00:00:00 2001 From: linyuh Date: Tue, 9 Jan 2018 18:26:39 -0800 Subject: Rename Cp2PhoneLookup as Cp2LocalPhoneLookup and PhoneLookupInfo.cp2_info as PhoneLookupInfo.cp2_local_info. To support remote CP2 contacts, there will be a new PhoneLookup ("Cp2RemotePhoneLookup") and a new field in proto PhoneLookupInfo ("cp2_remote_info"). In proto PhoneLookupInfo, cp2_local_info and cp2_remote_info will be of the same type ("Cp2Info"). Bug: 71763594 Test: Existing tests PiperOrigin-RevId: 181405798 Change-Id: I6c43b486229d4e9ae7b55c579d9c9997a2884c80 --- .../dialer/phonelookup/PhoneLookupModule.java | 6 +- .../phonelookup/cp2/Cp2LocalPhoneLookup.java | 982 +++++++++++++++++++++ .../dialer/phonelookup/cp2/Cp2PhoneLookup.java | 979 -------------------- .../dialer/phonelookup/phone_lookup_info.proto | 12 +- .../phonelookup/selector/PhoneLookupSelector.java | 6 +- 5 files changed, 994 insertions(+), 991 deletions(-) create mode 100644 java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java delete mode 100644 java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java (limited to 'java/com/android/dialer/phonelookup') diff --git a/java/com/android/dialer/phonelookup/PhoneLookupModule.java b/java/com/android/dialer/phonelookup/PhoneLookupModule.java index b4f37872e..5e215bafe 100644 --- a/java/com/android/dialer/phonelookup/PhoneLookupModule.java +++ b/java/com/android/dialer/phonelookup/PhoneLookupModule.java @@ -18,7 +18,7 @@ package com.android.dialer.phonelookup; import com.android.dialer.phonelookup.blockednumber.DialerBlockedNumberPhoneLookup; import com.android.dialer.phonelookup.composite.CompositePhoneLookup; -import com.android.dialer.phonelookup.cp2.Cp2PhoneLookup; +import com.android.dialer.phonelookup.cp2.Cp2LocalPhoneLookup; import com.google.common.collect.ImmutableList; import dagger.Module; import dagger.Provides; @@ -30,9 +30,9 @@ public abstract class PhoneLookupModule { @Provides @SuppressWarnings({"unchecked", "rawtype"}) static ImmutableList providePhoneLookupList( - Cp2PhoneLookup cp2PhoneLookup, + Cp2LocalPhoneLookup cp2LocalPhoneLookup, DialerBlockedNumberPhoneLookup dialerBlockedNumberPhoneLookup) { - return ImmutableList.of(cp2PhoneLookup, dialerBlockedNumberPhoneLookup); + return ImmutableList.of(cp2LocalPhoneLookup, dialerBlockedNumberPhoneLookup); } @Provides diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java new file mode 100644 index 000000000..727977f5c --- /dev/null +++ b/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java @@ -0,0 +1,982 @@ +/* + * 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.phonelookup.cp2; + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.DeletedContacts; +import android.support.annotation.Nullable; +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.LogUtil; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; +import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; +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.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info.Cp2ContactInfo; +import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; +import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; +import com.android.dialer.phonenumberproto.PartitionedNumbers; +import com.android.dialer.storage.Unencrypted; +import com.android.dialer.telecom.TelecomCallUtil; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.protobuf.InvalidProtocolBufferException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.Callable; +import javax.inject.Inject; + +/** PhoneLookup implementation for local contacts. */ +public final class Cp2LocalPhoneLookup implements PhoneLookup { + + private static final String PREF_LAST_TIMESTAMP_PROCESSED = + "cp2PhoneLookupLastTimestampProcessed"; + + /** Projection for performing batch lookups based on E164 numbers using the PHONE table. */ + private static final String[] PHONE_PROJECTION = + new String[] { + Phone.DISPLAY_NAME_PRIMARY, // 0 + Phone.PHOTO_THUMBNAIL_URI, // 1 + Phone.PHOTO_ID, // 2 + Phone.TYPE, // 3 + Phone.LABEL, // 4 + Phone.NORMALIZED_NUMBER, // 5 + Phone.CONTACT_ID, // 6 + Phone.LOOKUP_KEY // 7 + }; + + /** + * Projection for performing individual lookups of non-E164 numbers using the PHONE_LOOKUP table. + */ + private static final String[] PHONE_LOOKUP_PROJECTION = + new String[] { + ContactsContract.PhoneLookup.DISPLAY_NAME_PRIMARY, // 0 + ContactsContract.PhoneLookup.PHOTO_THUMBNAIL_URI, // 1 + ContactsContract.PhoneLookup.PHOTO_ID, // 2 + ContactsContract.PhoneLookup.TYPE, // 3 + ContactsContract.PhoneLookup.LABEL, // 4 + ContactsContract.PhoneLookup.NORMALIZED_NUMBER, // 5 + ContactsContract.PhoneLookup.CONTACT_ID, // 6 + ContactsContract.PhoneLookup.LOOKUP_KEY // 7 + }; + + // The following indexes should match both PHONE_PROJECTION and PHONE_LOOKUP_PROJECTION above. + 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_TYPE_INDEX = 3; + private static final int CP2_INFO_LABEL_INDEX = 4; + private static final int CP2_INFO_NORMALIZED_NUMBER_INDEX = 5; + private static final int CP2_INFO_CONTACT_ID_INDEX = 6; + private static final int CP2_INFO_LOOKUP_KEY_INDEX = 7; + + // We cannot efficiently process invalid numbers because batch queries cannot be constructed which + // accomplish the necessary loose matching. We'll attempt to process a limited number of them, + // but if there are too many we fall back to querying CP2 at render time. + private static final int MAX_SUPPORTED_INVALID_NUMBERS = 5; + + private final Context appContext; + private final SharedPreferences sharedPreferences; + private final ListeningExecutorService backgroundExecutorService; + private final ListeningExecutorService lightweightExecutorService; + + @Nullable private Long currentLastTimestampProcessed; + + @Inject + Cp2LocalPhoneLookup( + @ApplicationContext Context appContext, + @Unencrypted SharedPreferences sharedPreferences, + @BackgroundExecutor ListeningExecutorService backgroundExecutorService, + @LightweightExecutor ListeningExecutorService lightweightExecutorService) { + this.appContext = appContext; + this.sharedPreferences = sharedPreferences; + this.backgroundExecutorService = backgroundExecutorService; + this.lightweightExecutorService = lightweightExecutorService; + } + + @Override + public ListenableFuture lookup(Call call) { + return backgroundExecutorService.submit(() -> lookupInternal(call)); + } + + private Cp2Info lookupInternal(Call call) { + String rawNumber = TelecomCallUtil.getNumber(call); + if (TextUtils.isEmpty(rawNumber)) { + return Cp2Info.getDefaultInstance(); + } + Optional e164 = TelecomCallUtil.getE164Number(appContext, call); + Set cp2ContactInfos = new ArraySet<>(); + // Note: It would make sense to use PHONE_LOOKUP for E164 numbers as well, but we use PHONE to + // ensure consistency when the batch methods are used to update data. + try (Cursor cursor = + e164.isPresent() + ? queryPhoneTableBasedOnE164(PHONE_PROJECTION, ImmutableSet.of(e164.get())) + : queryPhoneLookup(PHONE_LOOKUP_PROJECTION, rawNumber)) { + if (cursor == null) { + LogUtil.w("Cp2LocalPhoneLookup.lookupInternal", "null cursor"); + return Cp2Info.getDefaultInstance(); + } + while (cursor.moveToNext()) { + cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); + } + } + return Cp2Info.newBuilder().addAllCp2ContactInfo(cp2ContactInfos).build(); + } + + /** + * Queries ContactsContract.PhoneLookup for the {@link Cp2Info} associated with the provided + * {@link DialerPhoneNumber}. Returns {@link Cp2Info#getDefaultInstance()} if there is no + * information. + */ + public ListenableFuture lookupByNumber(DialerPhoneNumber dialerPhoneNumber) { + return backgroundExecutorService.submit( + () -> { + DialerPhoneNumberUtil dialerPhoneNumberUtil = + new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()); + String rawNumber = dialerPhoneNumberUtil.normalizeNumber(dialerPhoneNumber); + if (rawNumber.isEmpty()) { + return Cp2Info.getDefaultInstance(); + } + Set cp2ContactInfos = new ArraySet<>(); + try (Cursor cursor = queryPhoneLookup(PHONE_LOOKUP_PROJECTION, rawNumber)) { + if (cursor == null) { + LogUtil.w("Cp2LocalPhoneLookup.lookup", "null cursor"); + return Cp2Info.getDefaultInstance(); + } + while (cursor.moveToNext()) { + cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); + } + } + return Cp2Info.newBuilder().addAllCp2ContactInfo(cp2ContactInfos).build(); + }); + } + + @Override + public ListenableFuture isDirty(ImmutableSet phoneNumbers) { + PartitionedNumbers partitionedNumbers = new PartitionedNumbers(phoneNumbers); + if (partitionedNumbers.unformattableNumbers().size() > MAX_SUPPORTED_INVALID_NUMBERS) { + // If there are N invalid numbers, we can't determine determine dirtiness without running N + // queries; since running this many queries is not feasible for the (lightweight) isDirty + // check, simply return true. The expectation is that this should rarely be the case as the + // vast majority of numbers in call logs should be valid. + return Futures.immediateFuture(true); + } + + ListenableFuture lastModifiedFuture = + backgroundExecutorService.submit( + () -> sharedPreferences.getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L)); + return Futures.transformAsync( + lastModifiedFuture, + lastModified -> { + // We are always going to need to do this check and it is pretty cheap so do it first. + ListenableFuture anyContactsDeletedFuture = + anyContactsDeletedSince(lastModified); + return Futures.transformAsync( + anyContactsDeletedFuture, + anyContactsDeleted -> { + if (anyContactsDeleted) { + LogUtil.v( + "Cp2LocalPhoneLookup.isDirty", "returning true because contacts deleted"); + return Futures.immediateFuture(true); + } + // Hopefully the most common case is there are no contacts updated; we can detect + // this cheaply. + ListenableFuture noContactsModifiedSinceFuture = + noContactsModifiedSince(lastModified); + return Futures.transformAsync( + noContactsModifiedSinceFuture, + noContactsModifiedSince -> { + if (noContactsModifiedSince) { + LogUtil.v( + "Cp2LocalPhoneLookup.isDirty", + "returning false because no contacts modified since last run"); + return Futures.immediateFuture(false); + } + // This method is more expensive but is probably the most likely scenario; we + // are looking for changes to contacts which have been called. + ListenableFuture> contactIdsFuture = + queryPhoneTableForContactIds(phoneNumbers); + ListenableFuture contactsUpdatedFuture = + Futures.transformAsync( + contactIdsFuture, + contactIds -> contactsUpdated(contactIds, lastModified), + MoreExecutors.directExecutor()); + return Futures.transformAsync( + contactsUpdatedFuture, + contactsUpdated -> { + if (contactsUpdated) { + LogUtil.v( + "Cp2LocalPhoneLookup.isDirty", + "returning true because a previously called contact was updated"); + return Futures.immediateFuture(true); + } + // This is the most expensive method so do it last; the scenario is that + // a contact which has been called got disassociated with a number and + // we need to clear their information. + ListenableFuture> phoneLookupContactIdsFuture = + queryPhoneLookupHistoryForContactIds(); + return Futures.transformAsync( + phoneLookupContactIdsFuture, + phoneLookupContactIds -> + contactsUpdated(phoneLookupContactIds, lastModified), + MoreExecutors.directExecutor()); + }, + MoreExecutors.directExecutor()); + }, + MoreExecutors.directExecutor()); + }, + MoreExecutors.directExecutor()); + }, + MoreExecutors.directExecutor()); + } + + /** + * Returns set of contact ids that correspond to {@code dialerPhoneNumbers} if the contact exists. + */ + private ListenableFuture> queryPhoneTableForContactIds( + ImmutableSet dialerPhoneNumbers) { + PartitionedNumbers partitionedNumbers = new PartitionedNumbers(dialerPhoneNumbers); + + List>> queryFutures = new ArrayList<>(); + + // First use the E164 numbers to query the NORMALIZED_NUMBER column. + queryFutures.add( + queryPhoneTableForContactIdsBasedOnE164(partitionedNumbers.validE164Numbers())); + + // Then run a separate query for each invalid number. Separate queries are done to accomplish + // loose matching which couldn't be accomplished with a batch query. + Assert.checkState( + partitionedNumbers.unformattableNumbers().size() <= MAX_SUPPORTED_INVALID_NUMBERS); + for (String invalidNumber : partitionedNumbers.unformattableNumbers()) { + queryFutures.add(queryPhoneLookupTableForContactIdsBasedOnRawNumber(invalidNumber)); + } + return Futures.transform( + Futures.allAsList(queryFutures), + listOfSets -> { + Set contactIds = new ArraySet<>(); + for (Set ids : listOfSets) { + contactIds.addAll(ids); + } + return contactIds; + }, + lightweightExecutorService); + } + + /** Gets all of the contact ids from PhoneLookupHistory. */ + private ListenableFuture> queryPhoneLookupHistoryForContactIds() { + return backgroundExecutorService.submit( + () -> { + Set contactIds = new ArraySet<>(); + try (Cursor cursor = + appContext + .getContentResolver() + .query( + PhoneLookupHistory.CONTENT_URI, + new String[] { + PhoneLookupHistory.PHONE_LOOKUP_INFO, + }, + null, + null, + null)) { + + if (cursor == null) { + LogUtil.w("Cp2LocalPhoneLookup.queryPhoneLookupHistoryForContactIds", "null cursor"); + return contactIds; + } + + if (cursor.moveToFirst()) { + int phoneLookupInfoColumn = + cursor.getColumnIndexOrThrow(PhoneLookupHistory.PHONE_LOOKUP_INFO); + do { + PhoneLookupInfo phoneLookupInfo; + try { + phoneLookupInfo = + PhoneLookupInfo.parseFrom(cursor.getBlob(phoneLookupInfoColumn)); + } catch (InvalidProtocolBufferException e) { + throw new IllegalStateException(e); + } + for (Cp2ContactInfo info : + phoneLookupInfo.getCp2LocalInfo().getCp2ContactInfoList()) { + contactIds.add(info.getContactId()); + } + } while (cursor.moveToNext()); + } + } + return contactIds; + }); + } + + private ListenableFuture> queryPhoneTableForContactIdsBasedOnE164( + Set validE164Numbers) { + return backgroundExecutorService.submit( + () -> { + Set contactIds = new ArraySet<>(); + if (validE164Numbers.isEmpty()) { + return contactIds; + } + try (Cursor cursor = + queryPhoneTableBasedOnE164(new String[] {Phone.CONTACT_ID}, validE164Numbers)) { + if (cursor == null) { + LogUtil.w( + "Cp2LocalPhoneLookup.queryPhoneTableForContactIdsBasedOnE164", "null cursor"); + return contactIds; + } + while (cursor.moveToNext()) { + contactIds.add(cursor.getLong(0 /* columnIndex */)); + } + } + return contactIds; + }); + } + + private ListenableFuture> queryPhoneLookupTableForContactIdsBasedOnRawNumber( + String rawNumber) { + if (TextUtils.isEmpty(rawNumber)) { + return Futures.immediateFuture(new ArraySet<>()); + } + return backgroundExecutorService.submit( + () -> { + Set contactIds = new ArraySet<>(); + try (Cursor cursor = + queryPhoneLookup(new String[] {ContactsContract.PhoneLookup.CONTACT_ID}, rawNumber)) { + if (cursor == null) { + LogUtil.w( + "Cp2LocalPhoneLookup.queryPhoneLookupTableForContactIdsBasedOnRawNumber", + "null cursor"); + return contactIds; + } + while (cursor.moveToNext()) { + contactIds.add(cursor.getLong(0 /* columnIndex */)); + } + } + return contactIds; + }); + } + + /** Returns true if any contacts were modified after {@code lastModified}. */ + private ListenableFuture contactsUpdated(Set contactIds, long lastModified) { + return backgroundExecutorService.submit( + () -> { + try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) { + return cursor.getCount() > 0; + } + }); + } + + 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()) + + ")"; + + 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( + Contacts.CONTENT_URI, + new String[] {Contacts._ID, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP}, + where, + args, + null); + } + + private ListenableFuture noContactsModifiedSince(long lastModified) { + return backgroundExecutorService.submit( + () -> { + try (Cursor cursor = + appContext + .getContentResolver() + .query( + Contacts.CONTENT_URI, + new String[] {Contacts._ID}, + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?", + new String[] {Long.toString(lastModified)}, + Contacts._ID + " limit 1")) { + if (cursor == null) { + LogUtil.w("Cp2LocalPhoneLookup.noContactsModifiedSince", "null cursor"); + return false; + } + return cursor.getCount() == 0; + } + }); + } + + /** Returns true if any contacts were deleted after {@code lastModified}. */ + private ListenableFuture anyContactsDeletedSince(long lastModified) { + return backgroundExecutorService.submit( + () -> { + try (Cursor cursor = + appContext + .getContentResolver() + .query( + DeletedContacts.CONTENT_URI, + new String[] {DeletedContacts.CONTACT_DELETED_TIMESTAMP}, + DeletedContacts.CONTACT_DELETED_TIMESTAMP + " > ?", + new String[] {Long.toString(lastModified)}, + DeletedContacts.CONTACT_DELETED_TIMESTAMP + " limit 1")) { + if (cursor == null) { + LogUtil.w("Cp2LocalPhoneLookup.anyContactsDeletedSince", "null cursor"); + return false; + } + return cursor.getCount() > 0; + } + }); + } + + @Override + public void setSubMessage(PhoneLookupInfo.Builder destination, Cp2Info subMessage) { + destination.setCp2LocalInfo(subMessage); + } + + @Override + public Cp2Info getSubMessage(PhoneLookupInfo phoneLookupInfo) { + return phoneLookupInfo.getCp2LocalInfo(); + } + + @Override + public ListenableFuture> getMostRecentInfo( + ImmutableMap existingInfoMap) { + currentLastTimestampProcessed = null; + + ListenableFuture lastModifiedFuture = + backgroundExecutorService.submit( + () -> sharedPreferences.getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L)); + return Futures.transformAsync( + lastModifiedFuture, + lastModified -> { + // Build a set of each DialerPhoneNumber that was associated with a contact, and is no + // longer associated with that same contact. + ListenableFuture> deletedPhoneNumbersFuture = + getDeletedPhoneNumbers(existingInfoMap, lastModified); + + return Futures.transformAsync( + deletedPhoneNumbersFuture, + deletedPhoneNumbers -> { + + // If there are too many invalid numbers, just defer the work to render time. + ArraySet unprocessableNumbers = + findUnprocessableNumbers(existingInfoMap); + Map existingInfoMapToProcess = existingInfoMap; + if (!unprocessableNumbers.isEmpty()) { + existingInfoMapToProcess = + Maps.filterKeys( + existingInfoMap, number -> !unprocessableNumbers.contains(number)); + } + + // For each DialerPhoneNumber that was associated with a contact or added to a + // contact, build a map of those DialerPhoneNumbers to a set Cp2ContactInfos, where + // each Cp2ContactInfo represents a contact. + ListenableFuture>> + updatedContactsFuture = + buildMapForUpdatedOrAddedContacts( + existingInfoMapToProcess, lastModified, deletedPhoneNumbers); + + return Futures.transform( + updatedContactsFuture, + updatedContacts -> { + + // 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()) { + DialerPhoneNumber dialerPhoneNumber = entry.getKey(); + Cp2Info existingInfo = entry.getValue(); + + // Build off the existing info + Cp2Info.Builder infoBuilder = Cp2Info.newBuilder(existingInfo); + + // If the contact was updated, replace the Cp2ContactInfo list + if (updatedContacts.containsKey(dialerPhoneNumber)) { + infoBuilder + .clear() + .addAllCp2ContactInfo(updatedContacts.get(dialerPhoneNumber)); + // If it was deleted and not added to a new contact, clear all the CP2 + // information. + } else if (deletedPhoneNumbers.contains(dialerPhoneNumber)) { + infoBuilder.clear(); + } else if (unprocessableNumbers.contains(dialerPhoneNumber)) { + infoBuilder.clear().setIsIncomplete(true); + } + + // If the DialerPhoneNumber didn't change, add the unchanged existing info. + newInfoMapBuilder.put(dialerPhoneNumber, infoBuilder.build()); + } + return newInfoMapBuilder.build(); + }, + lightweightExecutorService); + }, + lightweightExecutorService); + }, + lightweightExecutorService); + } + + private ArraySet findUnprocessableNumbers( + ImmutableMap existingInfoMap) { + ArraySet unprocessableNumbers = new ArraySet<>(); + PartitionedNumbers partitionedNumbers = new PartitionedNumbers(existingInfoMap.keySet()); + if (partitionedNumbers.unformattableNumbers().size() > MAX_SUPPORTED_INVALID_NUMBERS) { + for (String invalidNumber : partitionedNumbers.unformattableNumbers()) { + unprocessableNumbers.addAll( + partitionedNumbers.dialerPhoneNumbersForUnformattable(invalidNumber)); + } + } + return unprocessableNumbers; + } + + @Override + public ListenableFuture onSuccessfulBulkUpdate() { + return backgroundExecutorService.submit( + () -> { + if (currentLastTimestampProcessed != null) { + sharedPreferences + .edit() + .putLong(PREF_LAST_TIMESTAMP_PROCESSED, currentLastTimestampProcessed) + .apply(); + } + return null; + }); + } + + private ListenableFuture> findNumbersToUpdate( + Map existingInfoMap, + long lastModified, + Set deletedPhoneNumbers) { + return backgroundExecutorService.submit( + () -> { + Set updatedNumbers = new ArraySet<>(); + Set contactIds = new ArraySet<>(); + for (Entry entry : existingInfoMap.entrySet()) { + DialerPhoneNumber dialerPhoneNumber = entry.getKey(); + Cp2Info existingInfo = entry.getValue(); + + // If the number was deleted, we need to check if it was added to a new contact. + if (deletedPhoneNumbers.contains(dialerPhoneNumber)) { + updatedNumbers.add(dialerPhoneNumber); + continue; + } + + // When the PhoneLookupHistory contains no information for a number, because for + // example the user just upgraded to the new UI, or cleared data, we need to check for + // updated info. + if (existingInfo.getCp2ContactInfoCount() == 0) { + updatedNumbers.add(dialerPhoneNumber); + } else { + // For each Cp2ContactInfo 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 (Cp2ContactInfo cp2ContactInfo : existingInfo.getCp2ContactInfoList()) { + long existingContactId = cp2ContactInfo.getContactId(); + if (existingContactId == 0) { + // If the number doesn't have a contact id, 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(dialerPhoneNumber); + } else { + contactIds.add(cp2ContactInfo.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. + if (!contactIds.isEmpty()) { + try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) { + int contactIdIndex = cursor.getColumnIndex(Contacts._ID); + int lastUpdatedIndex = cursor.getColumnIndex(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP); + 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 Cp2ContactInfo need to + // be updated. + long contactId = cursor.getLong(contactIdIndex); + updatedNumbers.addAll( + findDialerPhoneNumbersContainingContactId(existingInfoMap, contactId)); + long lastUpdatedTimestamp = cursor.getLong(lastUpdatedIndex); + if (currentLastTimestampProcessed == null + || currentLastTimestampProcessed < lastUpdatedTimestamp) { + currentLastTimestampProcessed = lastUpdatedTimestamp; + } + } + } + } + return updatedNumbers; + }); + } + + @Override + public void registerContentObservers( + Context appContext, ContentObserverCallbacks contentObserverCallbacks) { + // Do nothing since CP2 changes are too noisy. + } + + /** + * 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 Cp2ContactInfo + * + * @return Map of {@link DialerPhoneNumber} to {@link Cp2Info} with updated {@link + * Cp2ContactInfo}. + */ + private ListenableFuture>> + buildMapForUpdatedOrAddedContacts( + Map existingInfoMap, + long lastModified, + Set deletedPhoneNumbers) { + // Start by building a set of DialerPhoneNumbers that we want to update. + ListenableFuture> updatedNumbersFuture = + findNumbersToUpdate(existingInfoMap, lastModified, deletedPhoneNumbers); + + return Futures.transformAsync( + updatedNumbersFuture, + updatedNumbers -> { + if (updatedNumbers.isEmpty()) { + return Futures.immediateFuture(new ArrayMap<>()); + } + + // Divide the numbers into those we can format to E164 and those we can't. Issue a single + // batch query for the E164 numbers against the PHONE table, and in parallel issue + // individual queries against PHONE_LOOKUP for each non-E164 number. + // TODO(zachh): These queries are inefficient without a lastModified column to filter on. + PartitionedNumbers partitionedNumbers = + new PartitionedNumbers(ImmutableSet.copyOf(updatedNumbers)); + + ListenableFuture>> e164Future = + batchQueryForValidNumbers(partitionedNumbers.validE164Numbers()); + + List>> nonE164FuturesList = new ArrayList<>(); + for (String invalidNumber : partitionedNumbers.unformattableNumbers()) { + nonE164FuturesList.add(individualQueryForInvalidNumber(invalidNumber)); + } + + ListenableFuture>> nonE164Future = + Futures.allAsList(nonE164FuturesList); + + Callable>> computeMap = + () -> { + // These get() calls are safe because we are using whenAllSucceed below. + Map> e164Result = e164Future.get(); + List> non164Results = nonE164Future.get(); + + Map> map = new ArrayMap<>(); + + // First update the map with the E164 results. + for (Entry> entry : e164Result.entrySet()) { + String e164Number = entry.getKey(); + Set cp2ContactInfos = entry.getValue(); + + Set dialerPhoneNumbers = + partitionedNumbers.dialerPhoneNumbersForE164(e164Number); + + addInfo(map, dialerPhoneNumbers, cp2ContactInfos); + + // We are going to remove the numbers that we've handled so that we later can + // detect numbers that weren't handled and therefore need to have their contact + // information removed. + updatedNumbers.removeAll(dialerPhoneNumbers); + } + + // Next update the map with the non-E164 results. + int i = 0; + for (String unformattableNumber : partitionedNumbers.unformattableNumbers()) { + Set cp2Infos = non164Results.get(i++); + Set dialerPhoneNumbers = + partitionedNumbers.dialerPhoneNumbersForUnformattable(unformattableNumber); + + addInfo(map, dialerPhoneNumbers, cp2Infos); + + // We are going to remove the numbers that we've handled so that we later can + // detect numbers that weren't handled and therefore need to have their contact + // information removed. + updatedNumbers.removeAll(dialerPhoneNumbers); + } + + // The leftovers in updatedNumbers that weren't removed are numbers that were + // previously associated with contacts, but are no longer. Remove the contact + // information for them. + for (DialerPhoneNumber dialerPhoneNumber : updatedNumbers) { + map.put(dialerPhoneNumber, ImmutableSet.of()); + } + return map; + }; + return Futures.whenAllSucceed(e164Future, nonE164Future) + .call(computeMap, lightweightExecutorService); + }, + lightweightExecutorService); + } + + private ListenableFuture>> batchQueryForValidNumbers( + Set e164Numbers) { + return backgroundExecutorService.submit( + () -> { + Map> cp2ContactInfosByNumber = new ArrayMap<>(); + if (e164Numbers.isEmpty()) { + return cp2ContactInfosByNumber; + } + try (Cursor cursor = queryPhoneTableBasedOnE164(PHONE_PROJECTION, e164Numbers)) { + if (cursor == null) { + LogUtil.w("Cp2LocalPhoneLookup.batchQueryForValidNumbers", "null cursor"); + } else { + while (cursor.moveToNext()) { + String e164Number = cursor.getString(CP2_INFO_NORMALIZED_NUMBER_INDEX); + Set cp2ContactInfos = cp2ContactInfosByNumber.get(e164Number); + if (cp2ContactInfos == null) { + cp2ContactInfos = new ArraySet<>(); + cp2ContactInfosByNumber.put(e164Number, cp2ContactInfos); + } + cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); + } + } + } + return cp2ContactInfosByNumber; + }); + } + + private ListenableFuture> individualQueryForInvalidNumber( + String invalidNumber) { + return backgroundExecutorService.submit( + () -> { + Set cp2ContactInfos = new ArraySet<>(); + if (invalidNumber.isEmpty()) { + return cp2ContactInfos; + } + try (Cursor cursor = queryPhoneLookup(PHONE_LOOKUP_PROJECTION, invalidNumber)) { + if (cursor == null) { + LogUtil.w("Cp2LocalPhoneLookup.individualQueryForInvalidNumber", "null cursor"); + } else { + while (cursor.moveToNext()) { + cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); + } + } + } + return cp2ContactInfos; + }); + } + + /** + * Adds the {@code cp2ContactInfo} to the entries for all specified {@code dialerPhoneNumbers} in + * the {@code map}. + */ + private static void addInfo( + Map> map, + Set dialerPhoneNumbers, + Set cp2ContactInfos) { + for (DialerPhoneNumber dialerPhoneNumber : dialerPhoneNumbers) { + Set existingInfos = map.get(dialerPhoneNumber); + if (existingInfos == null) { + existingInfos = new ArraySet<>(); + map.put(dialerPhoneNumber, existingInfos); + } + existingInfos.addAll(cp2ContactInfos); + } + } + + private Cursor queryPhoneTableBasedOnE164(String[] projection, Set validE164Numbers) { + return appContext + .getContentResolver() + .query( + Phone.CONTENT_URI, + projection, + Phone.NORMALIZED_NUMBER + " IN (" + questionMarks(validE164Numbers.size()) + ")", + validE164Numbers.toArray(new String[validE164Numbers.size()]), + null); + } + + private Cursor queryPhoneLookup(String[] projection, String rawNumber) { + Uri uri = + Uri.withAppendedPath( + ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(rawNumber)); + return appContext.getContentResolver().query(uri, projection, null, null, null); + } + + /** + * @param cursor with projection {@link #PHONE_PROJECTION}. + * @return new {@link Cp2ContactInfo} based on current row of {@code cursor}. + */ + private static Cp2ContactInfo buildCp2ContactInfoFromPhoneCursor( + Context appContext, Cursor cursor) { + String displayName = cursor.getString(CP2_INFO_NAME_INDEX); + String photoUri = cursor.getString(CP2_INFO_PHOTO_URI_INDEX); + int photoId = cursor.getInt(CP2_INFO_PHOTO_ID_INDEX); + int type = cursor.getInt(CP2_INFO_TYPE_INDEX); + String label = cursor.getString(CP2_INFO_LABEL_INDEX); + int contactId = cursor.getInt(CP2_INFO_CONTACT_ID_INDEX); + String lookupKey = cursor.getString(CP2_INFO_LOOKUP_KEY_INDEX); + + Cp2ContactInfo.Builder infoBuilder = Cp2ContactInfo.newBuilder(); + if (!TextUtils.isEmpty(displayName)) { + infoBuilder.setName(displayName); + } + if (!TextUtils.isEmpty(photoUri)) { + infoBuilder.setPhotoUri(photoUri); + } + if (photoId > 0) { + infoBuilder.setPhotoId(photoId); + } + + // Phone.getTypeLabel returns "Custom" if given (0, null) which is not of any use. Just + // omit setting the label if there's no information for it. + if (type != 0 || !TextUtils.isEmpty(label)) { + infoBuilder.setLabel(Phone.getTypeLabel(appContext.getResources(), type, label).toString()); + } + infoBuilder.setContactId(contactId); + if (!TextUtils.isEmpty(lookupKey)) { + infoBuilder.setLookupUri(Contacts.getLookupUri(contactId, lookupKey).toString()); + } + return infoBuilder.build(); + } + + /** Returns set of DialerPhoneNumbers that were associated with now deleted contacts. */ + private ListenableFuture> getDeletedPhoneNumbers( + ImmutableMap existingInfoMap, long lastModified) { + return backgroundExecutorService.submit( + () -> { + // 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 (Cp2Info info : map.values()) { + for (Cp2ContactInfo cp2ContactInfo : info.getCp2ContactInfoList()) { + contactIds.add(cp2ContactInfo.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, DeletedContacts.CONTACT_DELETED_TIMESTAMP}, + 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); + int deletedTimeIndex = cursor.getColumnIndexOrThrow(DeletedContacts.CONTACT_DELETED_TIMESTAMP); + Set deletedPhoneNumbers = new ArraySet<>(); + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + long contactId = cursor.getLong(contactIdIndex); + deletedPhoneNumbers.addAll( + findDialerPhoneNumbersContainingContactId(existingInfoMap, contactId)); + long deletedTime = cursor.getLong(deletedTimeIndex); + if (currentLastTimestampProcessed == null || currentLastTimestampProcessed < deletedTime) { + // TODO(zachh): There's a problem here if a contact for a new row is deleted? + currentLastTimestampProcessed = deletedTime; + } + } + return deletedPhoneNumbers; + } + + private static Set findDialerPhoneNumbersContainingContactId( + Map existingInfoMap, long contactId) { + Set matches = new ArraySet<>(); + for (Entry entry : existingInfoMap.entrySet()) { + for (Cp2ContactInfo cp2ContactInfo : entry.getValue().getCp2ContactInfoList()) { + if (cp2ContactInfo.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(); + for (int i = 0; i < count; i++) { + if (i != 0) { + where.append(", "); + } + where.append("?"); + } + return where.toString(); + } +} diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java deleted file mode 100644 index dd440708c..000000000 --- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java +++ /dev/null @@ -1,979 +0,0 @@ -/* - * 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.phonelookup.cp2; - -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.DeletedContacts; -import android.support.annotation.Nullable; -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.LogUtil; -import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; -import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; -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.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info.Cp2ContactInfo; -import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; -import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; -import com.android.dialer.phonenumberproto.PartitionedNumbers; -import com.android.dialer.storage.Unencrypted; -import com.android.dialer.telecom.TelecomCallUtil; -import com.google.common.base.Optional; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Maps; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.i18n.phonenumbers.PhoneNumberUtil; -import com.google.protobuf.InvalidProtocolBufferException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.Callable; -import javax.inject.Inject; - -/** PhoneLookup implementation for local contacts. */ -public final class Cp2PhoneLookup implements PhoneLookup { - - private static final String PREF_LAST_TIMESTAMP_PROCESSED = - "cp2PhoneLookupLastTimestampProcessed"; - - /** Projection for performing batch lookups based on E164 numbers using the PHONE table. */ - private static final String[] PHONE_PROJECTION = - new String[] { - Phone.DISPLAY_NAME_PRIMARY, // 0 - Phone.PHOTO_THUMBNAIL_URI, // 1 - Phone.PHOTO_ID, // 2 - Phone.TYPE, // 3 - Phone.LABEL, // 4 - Phone.NORMALIZED_NUMBER, // 5 - Phone.CONTACT_ID, // 6 - Phone.LOOKUP_KEY // 7 - }; - - /** - * Projection for performing individual lookups of non-E164 numbers using the PHONE_LOOKUP table. - */ - private static final String[] PHONE_LOOKUP_PROJECTION = - new String[] { - ContactsContract.PhoneLookup.DISPLAY_NAME_PRIMARY, // 0 - ContactsContract.PhoneLookup.PHOTO_THUMBNAIL_URI, // 1 - ContactsContract.PhoneLookup.PHOTO_ID, // 2 - ContactsContract.PhoneLookup.TYPE, // 3 - ContactsContract.PhoneLookup.LABEL, // 4 - ContactsContract.PhoneLookup.NORMALIZED_NUMBER, // 5 - ContactsContract.PhoneLookup.CONTACT_ID, // 6 - ContactsContract.PhoneLookup.LOOKUP_KEY // 7 - }; - - // The following indexes should match both PHONE_PROJECTION and PHONE_LOOKUP_PROJECTION above. - 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_TYPE_INDEX = 3; - private static final int CP2_INFO_LABEL_INDEX = 4; - private static final int CP2_INFO_NORMALIZED_NUMBER_INDEX = 5; - private static final int CP2_INFO_CONTACT_ID_INDEX = 6; - private static final int CP2_INFO_LOOKUP_KEY_INDEX = 7; - - // We cannot efficiently process invalid numbers because batch queries cannot be constructed which - // accomplish the necessary loose matching. We'll attempt to process a limited number of them, - // but if there are too many we fall back to querying CP2 at render time. - private static final int MAX_SUPPORTED_INVALID_NUMBERS = 5; - - private final Context appContext; - private final SharedPreferences sharedPreferences; - private final ListeningExecutorService backgroundExecutorService; - private final ListeningExecutorService lightweightExecutorService; - - @Nullable private Long currentLastTimestampProcessed; - - @Inject - Cp2PhoneLookup( - @ApplicationContext Context appContext, - @Unencrypted SharedPreferences sharedPreferences, - @BackgroundExecutor ListeningExecutorService backgroundExecutorService, - @LightweightExecutor ListeningExecutorService lightweightExecutorService) { - this.appContext = appContext; - this.sharedPreferences = sharedPreferences; - this.backgroundExecutorService = backgroundExecutorService; - this.lightweightExecutorService = lightweightExecutorService; - } - - @Override - public ListenableFuture lookup(Call call) { - return backgroundExecutorService.submit(() -> lookupInternal(call)); - } - - private Cp2Info lookupInternal(Call call) { - String rawNumber = TelecomCallUtil.getNumber(call); - if (TextUtils.isEmpty(rawNumber)) { - return Cp2Info.getDefaultInstance(); - } - Optional e164 = TelecomCallUtil.getE164Number(appContext, call); - Set cp2ContactInfos = new ArraySet<>(); - // Note: It would make sense to use PHONE_LOOKUP for E164 numbers as well, but we use PHONE to - // ensure consistency when the batch methods are used to update data. - try (Cursor cursor = - e164.isPresent() - ? queryPhoneTableBasedOnE164(PHONE_PROJECTION, ImmutableSet.of(e164.get())) - : queryPhoneLookup(PHONE_LOOKUP_PROJECTION, rawNumber)) { - if (cursor == null) { - LogUtil.w("Cp2PhoneLookup.lookupInternal", "null cursor"); - return Cp2Info.getDefaultInstance(); - } - while (cursor.moveToNext()) { - cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); - } - } - return Cp2Info.newBuilder().addAllCp2ContactInfo(cp2ContactInfos).build(); - } - - /** - * Queries ContactsContract.PhoneLookup for the {@link Cp2Info} associated with the provided - * {@link DialerPhoneNumber}. Returns {@link Cp2Info#getDefaultInstance()} if there is no - * information. - */ - public ListenableFuture lookupByNumber(DialerPhoneNumber dialerPhoneNumber) { - return backgroundExecutorService.submit( - () -> { - DialerPhoneNumberUtil dialerPhoneNumberUtil = - new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()); - String rawNumber = dialerPhoneNumberUtil.normalizeNumber(dialerPhoneNumber); - if (rawNumber.isEmpty()) { - return Cp2Info.getDefaultInstance(); - } - Set cp2ContactInfos = new ArraySet<>(); - try (Cursor cursor = queryPhoneLookup(PHONE_LOOKUP_PROJECTION, rawNumber)) { - if (cursor == null) { - LogUtil.w("Cp2PhoneLookup.lookup", "null cursor"); - return Cp2Info.getDefaultInstance(); - } - while (cursor.moveToNext()) { - cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); - } - } - return Cp2Info.newBuilder().addAllCp2ContactInfo(cp2ContactInfos).build(); - }); - } - - @Override - public ListenableFuture isDirty(ImmutableSet phoneNumbers) { - PartitionedNumbers partitionedNumbers = new PartitionedNumbers(phoneNumbers); - if (partitionedNumbers.unformattableNumbers().size() > MAX_SUPPORTED_INVALID_NUMBERS) { - // If there are N invalid numbers, we can't determine determine dirtiness without running N - // queries; since running this many queries is not feasible for the (lightweight) isDirty - // check, simply return true. The expectation is that this should rarely be the case as the - // vast majority of numbers in call logs should be valid. - return Futures.immediateFuture(true); - } - - ListenableFuture lastModifiedFuture = - backgroundExecutorService.submit( - () -> sharedPreferences.getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L)); - return Futures.transformAsync( - lastModifiedFuture, - lastModified -> { - // We are always going to need to do this check and it is pretty cheap so do it first. - ListenableFuture anyContactsDeletedFuture = - anyContactsDeletedSince(lastModified); - return Futures.transformAsync( - anyContactsDeletedFuture, - anyContactsDeleted -> { - if (anyContactsDeleted) { - LogUtil.v("Cp2PhoneLookup.isDirty", "returning true because contacts deleted"); - return Futures.immediateFuture(true); - } - // Hopefully the most common case is there are no contacts updated; we can detect - // this cheaply. - ListenableFuture noContactsModifiedSinceFuture = - noContactsModifiedSince(lastModified); - return Futures.transformAsync( - noContactsModifiedSinceFuture, - noContactsModifiedSince -> { - if (noContactsModifiedSince) { - LogUtil.v( - "Cp2PhoneLookup.isDirty", - "returning false because no contacts modified since last run"); - return Futures.immediateFuture(false); - } - // This method is more expensive but is probably the most likely scenario; we - // are looking for changes to contacts which have been called. - ListenableFuture> contactIdsFuture = - queryPhoneTableForContactIds(phoneNumbers); - ListenableFuture contactsUpdatedFuture = - Futures.transformAsync( - contactIdsFuture, - contactIds -> contactsUpdated(contactIds, lastModified), - MoreExecutors.directExecutor()); - return Futures.transformAsync( - contactsUpdatedFuture, - contactsUpdated -> { - if (contactsUpdated) { - LogUtil.v( - "Cp2PhoneLookup.isDirty", - "returning true because a previously called contact was updated"); - return Futures.immediateFuture(true); - } - // This is the most expensive method so do it last; the scenario is that - // a contact which has been called got disassociated with a number and - // we need to clear their information. - ListenableFuture> phoneLookupContactIdsFuture = - queryPhoneLookupHistoryForContactIds(); - return Futures.transformAsync( - phoneLookupContactIdsFuture, - phoneLookupContactIds -> - contactsUpdated(phoneLookupContactIds, lastModified), - MoreExecutors.directExecutor()); - }, - MoreExecutors.directExecutor()); - }, - MoreExecutors.directExecutor()); - }, - MoreExecutors.directExecutor()); - }, - MoreExecutors.directExecutor()); - } - - /** - * Returns set of contact ids that correspond to {@code dialerPhoneNumbers} if the contact exists. - */ - private ListenableFuture> queryPhoneTableForContactIds( - ImmutableSet dialerPhoneNumbers) { - PartitionedNumbers partitionedNumbers = new PartitionedNumbers(dialerPhoneNumbers); - - List>> queryFutures = new ArrayList<>(); - - // First use the E164 numbers to query the NORMALIZED_NUMBER column. - queryFutures.add( - queryPhoneTableForContactIdsBasedOnE164(partitionedNumbers.validE164Numbers())); - - // Then run a separate query for each invalid number. Separate queries are done to accomplish - // loose matching which couldn't be accomplished with a batch query. - Assert.checkState( - partitionedNumbers.unformattableNumbers().size() <= MAX_SUPPORTED_INVALID_NUMBERS); - for (String invalidNumber : partitionedNumbers.unformattableNumbers()) { - queryFutures.add(queryPhoneLookupTableForContactIdsBasedOnRawNumber(invalidNumber)); - } - return Futures.transform( - Futures.allAsList(queryFutures), - listOfSets -> { - Set contactIds = new ArraySet<>(); - for (Set ids : listOfSets) { - contactIds.addAll(ids); - } - return contactIds; - }, - lightweightExecutorService); - } - - /** Gets all of the contact ids from PhoneLookupHistory. */ - private ListenableFuture> queryPhoneLookupHistoryForContactIds() { - return backgroundExecutorService.submit( - () -> { - Set contactIds = new ArraySet<>(); - try (Cursor cursor = - appContext - .getContentResolver() - .query( - PhoneLookupHistory.CONTENT_URI, - new String[] { - PhoneLookupHistory.PHONE_LOOKUP_INFO, - }, - null, - null, - null)) { - - if (cursor == null) { - LogUtil.w("Cp2PhoneLookup.queryPhoneLookupHistoryForContactIds", "null cursor"); - return contactIds; - } - - if (cursor.moveToFirst()) { - int phoneLookupInfoColumn = - cursor.getColumnIndexOrThrow(PhoneLookupHistory.PHONE_LOOKUP_INFO); - do { - PhoneLookupInfo phoneLookupInfo; - try { - phoneLookupInfo = - PhoneLookupInfo.parseFrom(cursor.getBlob(phoneLookupInfoColumn)); - } catch (InvalidProtocolBufferException e) { - throw new IllegalStateException(e); - } - for (Cp2ContactInfo info : phoneLookupInfo.getCp2Info().getCp2ContactInfoList()) { - contactIds.add(info.getContactId()); - } - } while (cursor.moveToNext()); - } - } - return contactIds; - }); - } - - private ListenableFuture> queryPhoneTableForContactIdsBasedOnE164( - Set validE164Numbers) { - return backgroundExecutorService.submit( - () -> { - Set contactIds = new ArraySet<>(); - if (validE164Numbers.isEmpty()) { - return contactIds; - } - try (Cursor cursor = - queryPhoneTableBasedOnE164(new String[] {Phone.CONTACT_ID}, validE164Numbers)) { - if (cursor == null) { - LogUtil.w("Cp2PhoneLookup.queryPhoneTableForContactIdsBasedOnE164", "null cursor"); - return contactIds; - } - while (cursor.moveToNext()) { - contactIds.add(cursor.getLong(0 /* columnIndex */)); - } - } - return contactIds; - }); - } - - private ListenableFuture> queryPhoneLookupTableForContactIdsBasedOnRawNumber( - String rawNumber) { - if (TextUtils.isEmpty(rawNumber)) { - return Futures.immediateFuture(new ArraySet<>()); - } - return backgroundExecutorService.submit( - () -> { - Set contactIds = new ArraySet<>(); - try (Cursor cursor = - queryPhoneLookup(new String[] {ContactsContract.PhoneLookup.CONTACT_ID}, rawNumber)) { - if (cursor == null) { - LogUtil.w( - "Cp2PhoneLookup.queryPhoneLookupTableForContactIdsBasedOnRawNumber", - "null cursor"); - return contactIds; - } - while (cursor.moveToNext()) { - contactIds.add(cursor.getLong(0 /* columnIndex */)); - } - } - return contactIds; - }); - } - - /** Returns true if any contacts were modified after {@code lastModified}. */ - private ListenableFuture contactsUpdated(Set contactIds, long lastModified) { - return backgroundExecutorService.submit( - () -> { - try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) { - return cursor.getCount() > 0; - } - }); - } - - 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()) - + ")"; - - 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( - Contacts.CONTENT_URI, - new String[] {Contacts._ID, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP}, - where, - args, - null); - } - - private ListenableFuture noContactsModifiedSince(long lastModified) { - return backgroundExecutorService.submit( - () -> { - try (Cursor cursor = - appContext - .getContentResolver() - .query( - Contacts.CONTENT_URI, - new String[] {Contacts._ID}, - Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?", - new String[] {Long.toString(lastModified)}, - Contacts._ID + " limit 1")) { - if (cursor == null) { - LogUtil.w("Cp2PhoneLookup.noContactsModifiedSince", "null cursor"); - return false; - } - return cursor.getCount() == 0; - } - }); - } - - /** Returns true if any contacts were deleted after {@code lastModified}. */ - private ListenableFuture anyContactsDeletedSince(long lastModified) { - return backgroundExecutorService.submit( - () -> { - try (Cursor cursor = - appContext - .getContentResolver() - .query( - DeletedContacts.CONTENT_URI, - new String[] {DeletedContacts.CONTACT_DELETED_TIMESTAMP}, - DeletedContacts.CONTACT_DELETED_TIMESTAMP + " > ?", - new String[] {Long.toString(lastModified)}, - DeletedContacts.CONTACT_DELETED_TIMESTAMP + " limit 1")) { - if (cursor == null) { - LogUtil.w("Cp2PhoneLookup.anyContactsDeletedSince", "null cursor"); - return false; - } - return cursor.getCount() > 0; - } - }); - } - - @Override - public void setSubMessage(PhoneLookupInfo.Builder destination, Cp2Info subMessage) { - destination.setCp2Info(subMessage); - } - - @Override - public Cp2Info getSubMessage(PhoneLookupInfo phoneLookupInfo) { - return phoneLookupInfo.getCp2Info(); - } - - @Override - public ListenableFuture> getMostRecentInfo( - ImmutableMap existingInfoMap) { - currentLastTimestampProcessed = null; - - ListenableFuture lastModifiedFuture = - backgroundExecutorService.submit( - () -> sharedPreferences.getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L)); - return Futures.transformAsync( - lastModifiedFuture, - lastModified -> { - // Build a set of each DialerPhoneNumber that was associated with a contact, and is no - // longer associated with that same contact. - ListenableFuture> deletedPhoneNumbersFuture = - getDeletedPhoneNumbers(existingInfoMap, lastModified); - - return Futures.transformAsync( - deletedPhoneNumbersFuture, - deletedPhoneNumbers -> { - - // If there are too many invalid numbers, just defer the work to render time. - ArraySet unprocessableNumbers = - findUnprocessableNumbers(existingInfoMap); - Map existingInfoMapToProcess = existingInfoMap; - if (!unprocessableNumbers.isEmpty()) { - existingInfoMapToProcess = - Maps.filterKeys( - existingInfoMap, number -> !unprocessableNumbers.contains(number)); - } - - // For each DialerPhoneNumber that was associated with a contact or added to a - // contact, build a map of those DialerPhoneNumbers to a set Cp2ContactInfos, where - // each Cp2ContactInfo represents a contact. - ListenableFuture>> - updatedContactsFuture = - buildMapForUpdatedOrAddedContacts( - existingInfoMapToProcess, lastModified, deletedPhoneNumbers); - - return Futures.transform( - updatedContactsFuture, - updatedContacts -> { - - // 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()) { - DialerPhoneNumber dialerPhoneNumber = entry.getKey(); - Cp2Info existingInfo = entry.getValue(); - - // Build off the existing info - Cp2Info.Builder infoBuilder = Cp2Info.newBuilder(existingInfo); - - // If the contact was updated, replace the Cp2ContactInfo list - if (updatedContacts.containsKey(dialerPhoneNumber)) { - infoBuilder - .clear() - .addAllCp2ContactInfo(updatedContacts.get(dialerPhoneNumber)); - // If it was deleted and not added to a new contact, clear all the CP2 - // information. - } else if (deletedPhoneNumbers.contains(dialerPhoneNumber)) { - infoBuilder.clear(); - } else if (unprocessableNumbers.contains(dialerPhoneNumber)) { - infoBuilder.clear().setIsIncomplete(true); - } - - // If the DialerPhoneNumber didn't change, add the unchanged existing info. - newInfoMapBuilder.put(dialerPhoneNumber, infoBuilder.build()); - } - return newInfoMapBuilder.build(); - }, - lightweightExecutorService); - }, - lightweightExecutorService); - }, - lightweightExecutorService); - } - - private ArraySet findUnprocessableNumbers( - ImmutableMap existingInfoMap) { - ArraySet unprocessableNumbers = new ArraySet<>(); - PartitionedNumbers partitionedNumbers = new PartitionedNumbers(existingInfoMap.keySet()); - if (partitionedNumbers.unformattableNumbers().size() > MAX_SUPPORTED_INVALID_NUMBERS) { - for (String invalidNumber : partitionedNumbers.unformattableNumbers()) { - unprocessableNumbers.addAll( - partitionedNumbers.dialerPhoneNumbersForUnformattable(invalidNumber)); - } - } - return unprocessableNumbers; - } - - @Override - public ListenableFuture onSuccessfulBulkUpdate() { - return backgroundExecutorService.submit( - () -> { - if (currentLastTimestampProcessed != null) { - sharedPreferences - .edit() - .putLong(PREF_LAST_TIMESTAMP_PROCESSED, currentLastTimestampProcessed) - .apply(); - } - return null; - }); - } - - private ListenableFuture> findNumbersToUpdate( - Map existingInfoMap, - long lastModified, - Set deletedPhoneNumbers) { - return backgroundExecutorService.submit( - () -> { - Set updatedNumbers = new ArraySet<>(); - Set contactIds = new ArraySet<>(); - for (Entry entry : existingInfoMap.entrySet()) { - DialerPhoneNumber dialerPhoneNumber = entry.getKey(); - Cp2Info existingInfo = entry.getValue(); - - // If the number was deleted, we need to check if it was added to a new contact. - if (deletedPhoneNumbers.contains(dialerPhoneNumber)) { - updatedNumbers.add(dialerPhoneNumber); - continue; - } - - // When the PhoneLookupHistory contains no information for a number, because for - // example the user just upgraded to the new UI, or cleared data, we need to check for - // updated info. - if (existingInfo.getCp2ContactInfoCount() == 0) { - updatedNumbers.add(dialerPhoneNumber); - } else { - // For each Cp2ContactInfo 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 (Cp2ContactInfo cp2ContactInfo : existingInfo.getCp2ContactInfoList()) { - long existingContactId = cp2ContactInfo.getContactId(); - if (existingContactId == 0) { - // If the number doesn't have a contact id, 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(dialerPhoneNumber); - } else { - contactIds.add(cp2ContactInfo.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. - if (!contactIds.isEmpty()) { - try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) { - int contactIdIndex = cursor.getColumnIndex(Contacts._ID); - int lastUpdatedIndex = cursor.getColumnIndex(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP); - 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 Cp2ContactInfo need to - // be updated. - long contactId = cursor.getLong(contactIdIndex); - updatedNumbers.addAll( - findDialerPhoneNumbersContainingContactId(existingInfoMap, contactId)); - long lastUpdatedTimestamp = cursor.getLong(lastUpdatedIndex); - if (currentLastTimestampProcessed == null - || currentLastTimestampProcessed < lastUpdatedTimestamp) { - currentLastTimestampProcessed = lastUpdatedTimestamp; - } - } - } - } - return updatedNumbers; - }); - } - - @Override - public void registerContentObservers( - Context appContext, ContentObserverCallbacks contentObserverCallbacks) { - // Do nothing since CP2 changes are too noisy. - } - - /** - * 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 Cp2ContactInfo - * - * @return Map of {@link DialerPhoneNumber} to {@link Cp2Info} with updated {@link - * Cp2ContactInfo}. - */ - private ListenableFuture>> - buildMapForUpdatedOrAddedContacts( - Map existingInfoMap, - long lastModified, - Set deletedPhoneNumbers) { - // Start by building a set of DialerPhoneNumbers that we want to update. - ListenableFuture> updatedNumbersFuture = - findNumbersToUpdate(existingInfoMap, lastModified, deletedPhoneNumbers); - - return Futures.transformAsync( - updatedNumbersFuture, - updatedNumbers -> { - if (updatedNumbers.isEmpty()) { - return Futures.immediateFuture(new ArrayMap<>()); - } - - // Divide the numbers into those we can format to E164 and those we can't. Issue a single - // batch query for the E164 numbers against the PHONE table, and in parallel issue - // individual queries against PHONE_LOOKUP for each non-E164 number. - // TODO(zachh): These queries are inefficient without a lastModified column to filter on. - PartitionedNumbers partitionedNumbers = - new PartitionedNumbers(ImmutableSet.copyOf(updatedNumbers)); - - ListenableFuture>> e164Future = - batchQueryForValidNumbers(partitionedNumbers.validE164Numbers()); - - List>> nonE164FuturesList = new ArrayList<>(); - for (String invalidNumber : partitionedNumbers.unformattableNumbers()) { - nonE164FuturesList.add(individualQueryForInvalidNumber(invalidNumber)); - } - - ListenableFuture>> nonE164Future = - Futures.allAsList(nonE164FuturesList); - - Callable>> computeMap = - () -> { - // These get() calls are safe because we are using whenAllSucceed below. - Map> e164Result = e164Future.get(); - List> non164Results = nonE164Future.get(); - - Map> map = new ArrayMap<>(); - - // First update the map with the E164 results. - for (Entry> entry : e164Result.entrySet()) { - String e164Number = entry.getKey(); - Set cp2ContactInfos = entry.getValue(); - - Set dialerPhoneNumbers = - partitionedNumbers.dialerPhoneNumbersForE164(e164Number); - - addInfo(map, dialerPhoneNumbers, cp2ContactInfos); - - // We are going to remove the numbers that we've handled so that we later can - // detect numbers that weren't handled and therefore need to have their contact - // information removed. - updatedNumbers.removeAll(dialerPhoneNumbers); - } - - // Next update the map with the non-E164 results. - int i = 0; - for (String unformattableNumber : partitionedNumbers.unformattableNumbers()) { - Set cp2Infos = non164Results.get(i++); - Set dialerPhoneNumbers = - partitionedNumbers.dialerPhoneNumbersForUnformattable(unformattableNumber); - - addInfo(map, dialerPhoneNumbers, cp2Infos); - - // We are going to remove the numbers that we've handled so that we later can - // detect numbers that weren't handled and therefore need to have their contact - // information removed. - updatedNumbers.removeAll(dialerPhoneNumbers); - } - - // The leftovers in updatedNumbers that weren't removed are numbers that were - // previously associated with contacts, but are no longer. Remove the contact - // information for them. - for (DialerPhoneNumber dialerPhoneNumber : updatedNumbers) { - map.put(dialerPhoneNumber, ImmutableSet.of()); - } - return map; - }; - return Futures.whenAllSucceed(e164Future, nonE164Future) - .call(computeMap, lightweightExecutorService); - }, - lightweightExecutorService); - } - - private ListenableFuture>> batchQueryForValidNumbers( - Set e164Numbers) { - return backgroundExecutorService.submit( - () -> { - Map> cp2ContactInfosByNumber = new ArrayMap<>(); - if (e164Numbers.isEmpty()) { - return cp2ContactInfosByNumber; - } - try (Cursor cursor = queryPhoneTableBasedOnE164(PHONE_PROJECTION, e164Numbers)) { - if (cursor == null) { - LogUtil.w("Cp2PhoneLookup.batchQueryForValidNumbers", "null cursor"); - } else { - while (cursor.moveToNext()) { - String e164Number = cursor.getString(CP2_INFO_NORMALIZED_NUMBER_INDEX); - Set cp2ContactInfos = cp2ContactInfosByNumber.get(e164Number); - if (cp2ContactInfos == null) { - cp2ContactInfos = new ArraySet<>(); - cp2ContactInfosByNumber.put(e164Number, cp2ContactInfos); - } - cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); - } - } - } - return cp2ContactInfosByNumber; - }); - } - - private ListenableFuture> individualQueryForInvalidNumber( - String invalidNumber) { - return backgroundExecutorService.submit( - () -> { - Set cp2ContactInfos = new ArraySet<>(); - if (invalidNumber.isEmpty()) { - return cp2ContactInfos; - } - try (Cursor cursor = queryPhoneLookup(PHONE_LOOKUP_PROJECTION, invalidNumber)) { - if (cursor == null) { - LogUtil.w("Cp2PhoneLookup.individualQueryForInvalidNumber", "null cursor"); - } else { - while (cursor.moveToNext()) { - cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); - } - } - } - return cp2ContactInfos; - }); - } - - /** - * Adds the {@code cp2ContactInfo} to the entries for all specified {@code dialerPhoneNumbers} in - * the {@code map}. - */ - private static void addInfo( - Map> map, - Set dialerPhoneNumbers, - Set cp2ContactInfos) { - for (DialerPhoneNumber dialerPhoneNumber : dialerPhoneNumbers) { - Set existingInfos = map.get(dialerPhoneNumber); - if (existingInfos == null) { - existingInfos = new ArraySet<>(); - map.put(dialerPhoneNumber, existingInfos); - } - existingInfos.addAll(cp2ContactInfos); - } - } - - private Cursor queryPhoneTableBasedOnE164(String[] projection, Set validE164Numbers) { - return appContext - .getContentResolver() - .query( - Phone.CONTENT_URI, - projection, - Phone.NORMALIZED_NUMBER + " IN (" + questionMarks(validE164Numbers.size()) + ")", - validE164Numbers.toArray(new String[validE164Numbers.size()]), - null); - } - - private Cursor queryPhoneLookup(String[] projection, String rawNumber) { - Uri uri = - Uri.withAppendedPath( - ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(rawNumber)); - return appContext.getContentResolver().query(uri, projection, null, null, null); - } - - /** - * @param cursor with projection {@link #PHONE_PROJECTION}. - * @return new {@link Cp2ContactInfo} based on current row of {@code cursor}. - */ - private static Cp2ContactInfo buildCp2ContactInfoFromPhoneCursor( - Context appContext, Cursor cursor) { - String displayName = cursor.getString(CP2_INFO_NAME_INDEX); - String photoUri = cursor.getString(CP2_INFO_PHOTO_URI_INDEX); - int photoId = cursor.getInt(CP2_INFO_PHOTO_ID_INDEX); - int type = cursor.getInt(CP2_INFO_TYPE_INDEX); - String label = cursor.getString(CP2_INFO_LABEL_INDEX); - int contactId = cursor.getInt(CP2_INFO_CONTACT_ID_INDEX); - String lookupKey = cursor.getString(CP2_INFO_LOOKUP_KEY_INDEX); - - Cp2ContactInfo.Builder infoBuilder = Cp2ContactInfo.newBuilder(); - if (!TextUtils.isEmpty(displayName)) { - infoBuilder.setName(displayName); - } - if (!TextUtils.isEmpty(photoUri)) { - infoBuilder.setPhotoUri(photoUri); - } - if (photoId > 0) { - infoBuilder.setPhotoId(photoId); - } - - // Phone.getTypeLabel returns "Custom" if given (0, null) which is not of any use. Just - // omit setting the label if there's no information for it. - if (type != 0 || !TextUtils.isEmpty(label)) { - infoBuilder.setLabel(Phone.getTypeLabel(appContext.getResources(), type, label).toString()); - } - infoBuilder.setContactId(contactId); - if (!TextUtils.isEmpty(lookupKey)) { - infoBuilder.setLookupUri(Contacts.getLookupUri(contactId, lookupKey).toString()); - } - return infoBuilder.build(); - } - - /** Returns set of DialerPhoneNumbers that were associated with now deleted contacts. */ - private ListenableFuture> getDeletedPhoneNumbers( - ImmutableMap existingInfoMap, long lastModified) { - return backgroundExecutorService.submit( - () -> { - // 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 (Cp2Info info : map.values()) { - for (Cp2ContactInfo cp2ContactInfo : info.getCp2ContactInfoList()) { - contactIds.add(cp2ContactInfo.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, DeletedContacts.CONTACT_DELETED_TIMESTAMP}, - 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); - int deletedTimeIndex = cursor.getColumnIndexOrThrow(DeletedContacts.CONTACT_DELETED_TIMESTAMP); - Set deletedPhoneNumbers = new ArraySet<>(); - cursor.moveToPosition(-1); - while (cursor.moveToNext()) { - long contactId = cursor.getLong(contactIdIndex); - deletedPhoneNumbers.addAll( - findDialerPhoneNumbersContainingContactId(existingInfoMap, contactId)); - long deletedTime = cursor.getLong(deletedTimeIndex); - if (currentLastTimestampProcessed == null || currentLastTimestampProcessed < deletedTime) { - // TODO(zachh): There's a problem here if a contact for a new row is deleted? - currentLastTimestampProcessed = deletedTime; - } - } - return deletedPhoneNumbers; - } - - private static Set findDialerPhoneNumbersContainingContactId( - Map existingInfoMap, long contactId) { - Set matches = new ArraySet<>(); - for (Entry entry : existingInfoMap.entrySet()) { - for (Cp2ContactInfo cp2ContactInfo : entry.getValue().getCp2ContactInfoList()) { - if (cp2ContactInfo.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(); - for (int i = 0; i < count; i++) { - if (i != 0) { - where.append(", "); - } - where.append("?"); - } - 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 6662646aa..f1497bdca 100644 --- a/java/com/android/dialer/phonelookup/phone_lookup_info.proto +++ b/java/com/android/dialer/phonelookup/phone_lookup_info.proto @@ -9,12 +9,12 @@ package com.android.dialer.phonelookup; // Contains information about a phone number, possibly from many sources. // -// This message is organized into sub-messages where each sub-message -// corresponds to an implementation of PhoneLookup. For example, the Cp2Info -// corresponds to Cp2PhoneLookup class, and the Cp2PhoneLookup class alone is -// responsible for populating its fields. +// This message is organized into sub-message fields where each one corresponds +// to an implementation of PhoneLookup. For example, field "cp2_local_info" +// corresponds to class Cp2LocalPhoneLookup, and class Cp2LocalPhoneLookup +// alone is responsible for populating it. message PhoneLookupInfo { - // Information about a PhoneNumber retrieved from CP2. Cp2PhoneLookup is + // Information about a PhoneNumber retrieved from CP2. Cp2LocalPhoneLookup is // responsible for populating the data in this message. message Cp2Info { // Information about a single local contact. @@ -50,7 +50,7 @@ message PhoneLookupInfo { // log needs to query for the CP2 information at render time. optional bool is_incomplete = 2; } - optional Cp2Info cp2_info = 1; + optional Cp2Info cp2_local_info = 1; // Message for APDL, a lookup for the proprietary Google dialer. message ApdlInfo { diff --git a/java/com/android/dialer/phonelookup/selector/PhoneLookupSelector.java b/java/com/android/dialer/phonelookup/selector/PhoneLookupSelector.java index a960d4e96..6b217e951 100644 --- a/java/com/android/dialer/phonelookup/selector/PhoneLookupSelector.java +++ b/java/com/android/dialer/phonelookup/selector/PhoneLookupSelector.java @@ -139,7 +139,7 @@ public final class PhoneLookupSelector { // matches that of a Cp2 (local) contact, and PeopleApiInfo will not be used to display // information like name, photo, etc. We should not allow the user to report the number in this // case as the info displayed is not from the People API. - if (phoneLookupInfo.getCp2Info().getCp2ContactInfoCount() > 0) { + if (phoneLookupInfo.getCp2LocalInfo().getCp2ContactInfoCount() > 0) { return false; } @@ -155,8 +155,8 @@ public final class PhoneLookupSelector { */ @Nullable private Cp2ContactInfo firstLocalContact(PhoneLookupInfo phoneLookupInfo) { - if (phoneLookupInfo.getCp2Info().getCp2ContactInfoCount() > 0) { - return phoneLookupInfo.getCp2Info().getCp2ContactInfo(0); + if (phoneLookupInfo.getCp2LocalInfo().getCp2ContactInfoCount() > 0) { + return phoneLookupInfo.getCp2LocalInfo().getCp2ContactInfo(0); } return null; } -- cgit v1.2.3