Home | History | Annotate | Download | only in cp2
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.dialer.phonelookup.cp2;
     18 
     19 import android.content.Context;
     20 import android.content.SharedPreferences;
     21 import android.database.Cursor;
     22 import android.net.Uri;
     23 import android.provider.ContactsContract;
     24 import android.provider.ContactsContract.CommonDataKinds.Phone;
     25 import android.provider.ContactsContract.Contacts;
     26 import android.provider.ContactsContract.DeletedContacts;
     27 import android.support.annotation.Nullable;
     28 import android.support.v4.util.ArrayMap;
     29 import android.support.v4.util.ArraySet;
     30 import android.text.TextUtils;
     31 import com.android.dialer.DialerPhoneNumber;
     32 import com.android.dialer.common.Assert;
     33 import com.android.dialer.common.LogUtil;
     34 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
     35 import com.android.dialer.common.concurrent.Annotations.LightweightExecutor;
     36 import com.android.dialer.inject.ApplicationContext;
     37 import com.android.dialer.phonelookup.PhoneLookup;
     38 import com.android.dialer.phonelookup.PhoneLookupInfo;
     39 import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info;
     40 import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info.Cp2ContactInfo;
     41 import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory;
     42 import com.android.dialer.phonenumberproto.PartitionedNumbers;
     43 import com.android.dialer.storage.Unencrypted;
     44 import com.google.common.collect.ImmutableMap;
     45 import com.google.common.collect.ImmutableSet;
     46 import com.google.common.collect.Iterables;
     47 import com.google.common.collect.Maps;
     48 import com.google.common.util.concurrent.Futures;
     49 import com.google.common.util.concurrent.ListenableFuture;
     50 import com.google.common.util.concurrent.ListeningExecutorService;
     51 import com.google.common.util.concurrent.MoreExecutors;
     52 import com.google.protobuf.InvalidProtocolBufferException;
     53 import java.util.ArrayList;
     54 import java.util.List;
     55 import java.util.Map;
     56 import java.util.Map.Entry;
     57 import java.util.Set;
     58 import java.util.concurrent.Callable;
     59 import javax.inject.Inject;
     60 
     61 /** PhoneLookup implementation for contacts in the default directory. */
     62 public final class Cp2DefaultDirectoryPhoneLookup implements PhoneLookup<Cp2Info> {
     63 
     64   private static final String PREF_LAST_TIMESTAMP_PROCESSED =
     65       "cp2DefaultDirectoryPhoneLookupLastTimestampProcessed";
     66 
     67   // We cannot efficiently process invalid numbers because batch queries cannot be constructed which
     68   // accomplish the necessary loose matching. We'll attempt to process a limited number of them,
     69   // but if there are too many we fall back to querying CP2 at render time.
     70   private static final int MAX_SUPPORTED_INVALID_NUMBERS = 5;
     71 
     72   private final Context appContext;
     73   private final SharedPreferences sharedPreferences;
     74   private final ListeningExecutorService backgroundExecutorService;
     75   private final ListeningExecutorService lightweightExecutorService;
     76 
     77   @Nullable private Long currentLastTimestampProcessed;
     78 
     79   @Inject
     80   Cp2DefaultDirectoryPhoneLookup(
     81       @ApplicationContext Context appContext,
     82       @Unencrypted SharedPreferences sharedPreferences,
     83       @BackgroundExecutor ListeningExecutorService backgroundExecutorService,
     84       @LightweightExecutor ListeningExecutorService lightweightExecutorService) {
     85     this.appContext = appContext;
     86     this.sharedPreferences = sharedPreferences;
     87     this.backgroundExecutorService = backgroundExecutorService;
     88     this.lightweightExecutorService = lightweightExecutorService;
     89   }
     90 
     91   @Override
     92   public ListenableFuture<Cp2Info> lookup(DialerPhoneNumber dialerPhoneNumber) {
     93     return backgroundExecutorService.submit(() -> lookupInternal(dialerPhoneNumber));
     94   }
     95 
     96   private Cp2Info lookupInternal(DialerPhoneNumber dialerPhoneNumber) {
     97     String number = dialerPhoneNumber.getNormalizedNumber();
     98     if (TextUtils.isEmpty(number)) {
     99       return Cp2Info.getDefaultInstance();
    100     }
    101 
    102     Set<Cp2ContactInfo> cp2ContactInfos = new ArraySet<>();
    103 
    104     // Even though this is only a single number, use PartitionedNumbers to mimic the logic used
    105     // during getMostRecentInfo.
    106     PartitionedNumbers partitionedNumbers =
    107         new PartitionedNumbers(ImmutableSet.of(dialerPhoneNumber));
    108 
    109     Cursor cursor = null;
    110     try {
    111       // Note: It would make sense to use PHONE_LOOKUP for valid numbers as well, but we use PHONE
    112       // to ensure consistency when the batch methods are used to update data.
    113       if (!partitionedNumbers.validE164Numbers().isEmpty()) {
    114         cursor =
    115             queryPhoneTableBasedOnE164(
    116                 Cp2Projections.getProjectionForPhoneTable(), partitionedNumbers.validE164Numbers());
    117       } else {
    118         cursor =
    119             queryPhoneLookup(
    120                 Cp2Projections.getProjectionForPhoneLookupTable(),
    121                 Iterables.getOnlyElement(partitionedNumbers.invalidNumbers()));
    122       }
    123       if (cursor == null) {
    124         LogUtil.w("Cp2DefaultDirectoryPhoneLookup.lookupInternal", "null cursor");
    125         return Cp2Info.getDefaultInstance();
    126       }
    127       while (cursor.moveToNext()) {
    128         cp2ContactInfos.add(Cp2Projections.buildCp2ContactInfoFromCursor(appContext, cursor));
    129       }
    130     } finally {
    131       if (cursor != null) {
    132         cursor.close();
    133       }
    134     }
    135     return Cp2Info.newBuilder().addAllCp2ContactInfo(cp2ContactInfos).build();
    136   }
    137 
    138   @Override
    139   public ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers) {
    140     PartitionedNumbers partitionedNumbers = new PartitionedNumbers(phoneNumbers);
    141     if (partitionedNumbers.invalidNumbers().size() > MAX_SUPPORTED_INVALID_NUMBERS) {
    142       // If there are N invalid numbers, we can't determine determine dirtiness without running N
    143       // queries; since running this many queries is not feasible for the (lightweight) isDirty
    144       // check, simply return true. The expectation is that this should rarely be the case as the
    145       // vast majority of numbers in call logs should be valid.
    146       LogUtil.v(
    147           "Cp2DefaultDirectoryPhoneLookup.isDirty",
    148           "returning true because too many invalid numbers (%d)",
    149           partitionedNumbers.invalidNumbers().size());
    150       return Futures.immediateFuture(true);
    151     }
    152 
    153     ListenableFuture<Long> lastModifiedFuture =
    154         backgroundExecutorService.submit(
    155             () -> sharedPreferences.getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L));
    156     return Futures.transformAsync(
    157         lastModifiedFuture,
    158         lastModified -> {
    159           // We are always going to need to do this check and it is pretty cheap so do it first.
    160           ListenableFuture<Boolean> anyContactsDeletedFuture =
    161               anyContactsDeletedSince(lastModified);
    162           return Futures.transformAsync(
    163               anyContactsDeletedFuture,
    164               anyContactsDeleted -> {
    165                 if (anyContactsDeleted) {
    166                   LogUtil.v(
    167                       "Cp2DefaultDirectoryPhoneLookup.isDirty",
    168                       "returning true because contacts deleted");
    169                   return Futures.immediateFuture(true);
    170                 }
    171                 // Hopefully the most common case is there are no contacts updated; we can detect
    172                 // this cheaply.
    173                 ListenableFuture<Boolean> noContactsModifiedSinceFuture =
    174                     noContactsModifiedSince(lastModified);
    175                 return Futures.transformAsync(
    176                     noContactsModifiedSinceFuture,
    177                     noContactsModifiedSince -> {
    178                       if (noContactsModifiedSince) {
    179                         LogUtil.v(
    180                             "Cp2DefaultDirectoryPhoneLookup.isDirty",
    181                             "returning false because no contacts modified since last run");
    182                         return Futures.immediateFuture(false);
    183                       }
    184                       // This method is more expensive but is probably the most likely scenario; we
    185                       // are looking for changes to contacts which have been called.
    186                       ListenableFuture<Set<Long>> contactIdsFuture =
    187                           queryPhoneTableForContactIds(phoneNumbers);
    188                       ListenableFuture<Boolean> contactsUpdatedFuture =
    189                           Futures.transformAsync(
    190                               contactIdsFuture,
    191                               contactIds -> contactsUpdated(contactIds, lastModified),
    192                               MoreExecutors.directExecutor());
    193                       return Futures.transformAsync(
    194                           contactsUpdatedFuture,
    195                           contactsUpdated -> {
    196                             if (contactsUpdated) {
    197                               LogUtil.v(
    198                                   "Cp2DefaultDirectoryPhoneLookup.isDirty",
    199                                   "returning true because a previously called contact was updated");
    200                               return Futures.immediateFuture(true);
    201                             }
    202                             // This is the most expensive method so do it last; the scenario is that
    203                             // a contact which has been called got disassociated with a number and
    204                             // we need to clear their information.
    205                             ListenableFuture<Set<Long>> phoneLookupContactIdsFuture =
    206                                 queryPhoneLookupHistoryForContactIds();
    207                             return Futures.transformAsync(
    208                                 phoneLookupContactIdsFuture,
    209                                 phoneLookupContactIds ->
    210                                     contactsUpdated(phoneLookupContactIds, lastModified),
    211                                 MoreExecutors.directExecutor());
    212                           },
    213                           MoreExecutors.directExecutor());
    214                     },
    215                     MoreExecutors.directExecutor());
    216               },
    217               MoreExecutors.directExecutor());
    218         },
    219         MoreExecutors.directExecutor());
    220   }
    221 
    222   /**
    223    * Returns set of contact ids that correspond to {@code dialerPhoneNumbers} if the contact exists.
    224    */
    225   private ListenableFuture<Set<Long>> queryPhoneTableForContactIds(
    226       ImmutableSet<DialerPhoneNumber> dialerPhoneNumbers) {
    227     PartitionedNumbers partitionedNumbers = new PartitionedNumbers(dialerPhoneNumbers);
    228 
    229     List<ListenableFuture<Set<Long>>> queryFutures = new ArrayList<>();
    230 
    231     // First use the valid E164 numbers to query the NORMALIZED_NUMBER column.
    232     queryFutures.add(
    233         queryPhoneTableForContactIdsBasedOnE164(partitionedNumbers.validE164Numbers()));
    234 
    235     // Then run a separate query for each invalid number. Separate queries are done to accomplish
    236     // loose matching which couldn't be accomplished with a batch query.
    237     Assert.checkState(partitionedNumbers.invalidNumbers().size() <= MAX_SUPPORTED_INVALID_NUMBERS);
    238     for (String invalidNumber : partitionedNumbers.invalidNumbers()) {
    239       queryFutures.add(queryPhoneLookupTableForContactIdsBasedOnRawNumber(invalidNumber));
    240     }
    241     return Futures.transform(
    242         Futures.allAsList(queryFutures),
    243         listOfSets -> {
    244           Set<Long> contactIds = new ArraySet<>();
    245           for (Set<Long> ids : listOfSets) {
    246             contactIds.addAll(ids);
    247           }
    248           return contactIds;
    249         },
    250         lightweightExecutorService);
    251   }
    252 
    253   /** Gets all of the contact ids from PhoneLookupHistory. */
    254   private ListenableFuture<Set<Long>> queryPhoneLookupHistoryForContactIds() {
    255     return backgroundExecutorService.submit(
    256         () -> {
    257           Set<Long> contactIds = new ArraySet<>();
    258           try (Cursor cursor =
    259               appContext
    260                   .getContentResolver()
    261                   .query(
    262                       PhoneLookupHistory.CONTENT_URI,
    263                       new String[] {
    264                         PhoneLookupHistory.PHONE_LOOKUP_INFO,
    265                       },
    266                       null,
    267                       null,
    268                       null)) {
    269 
    270             if (cursor == null) {
    271               LogUtil.w(
    272                   "Cp2DefaultDirectoryPhoneLookup.queryPhoneLookupHistoryForContactIds",
    273                   "null cursor");
    274               return contactIds;
    275             }
    276 
    277             if (cursor.moveToFirst()) {
    278               int phoneLookupInfoColumn =
    279                   cursor.getColumnIndexOrThrow(PhoneLookupHistory.PHONE_LOOKUP_INFO);
    280               do {
    281                 PhoneLookupInfo phoneLookupInfo;
    282                 try {
    283                   phoneLookupInfo =
    284                       PhoneLookupInfo.parseFrom(cursor.getBlob(phoneLookupInfoColumn));
    285                 } catch (InvalidProtocolBufferException e) {
    286                   throw new IllegalStateException(e);
    287                 }
    288                 for (Cp2ContactInfo info :
    289                     phoneLookupInfo.getDefaultCp2Info().getCp2ContactInfoList()) {
    290                   contactIds.add(info.getContactId());
    291                 }
    292               } while (cursor.moveToNext());
    293             }
    294           }
    295           return contactIds;
    296         });
    297   }
    298 
    299   private ListenableFuture<Set<Long>> queryPhoneTableForContactIdsBasedOnE164(
    300       Set<String> validE164Numbers) {
    301     return backgroundExecutorService.submit(
    302         () -> {
    303           Set<Long> contactIds = new ArraySet<>();
    304           if (validE164Numbers.isEmpty()) {
    305             return contactIds;
    306           }
    307           try (Cursor cursor =
    308               queryPhoneTableBasedOnE164(new String[] {Phone.CONTACT_ID}, validE164Numbers)) {
    309             if (cursor == null) {
    310               LogUtil.w(
    311                   "Cp2DefaultDirectoryPhoneLookup.queryPhoneTableForContactIdsBasedOnE164",
    312                   "null cursor");
    313               return contactIds;
    314             }
    315             while (cursor.moveToNext()) {
    316               contactIds.add(cursor.getLong(0 /* columnIndex */));
    317             }
    318           }
    319           return contactIds;
    320         });
    321   }
    322 
    323   private ListenableFuture<Set<Long>> queryPhoneLookupTableForContactIdsBasedOnRawNumber(
    324       String rawNumber) {
    325     if (TextUtils.isEmpty(rawNumber)) {
    326       return Futures.immediateFuture(new ArraySet<>());
    327     }
    328     return backgroundExecutorService.submit(
    329         () -> {
    330           Set<Long> contactIds = new ArraySet<>();
    331           try (Cursor cursor =
    332               queryPhoneLookup(new String[] {ContactsContract.PhoneLookup.CONTACT_ID}, rawNumber)) {
    333             if (cursor == null) {
    334               LogUtil.w(
    335                   "Cp2DefaultDirectoryPhoneLookup.queryPhoneLookupTableForContactIdsBasedOnRawNumber",
    336                   "null cursor");
    337               return contactIds;
    338             }
    339             while (cursor.moveToNext()) {
    340               contactIds.add(cursor.getLong(0 /* columnIndex */));
    341             }
    342           }
    343           return contactIds;
    344         });
    345   }
    346 
    347   /** Returns true if any contacts were modified after {@code lastModified}. */
    348   private ListenableFuture<Boolean> contactsUpdated(Set<Long> contactIds, long lastModified) {
    349     return backgroundExecutorService.submit(
    350         () -> {
    351           try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) {
    352             return cursor.getCount() > 0;
    353           }
    354         });
    355   }
    356 
    357   private Cursor queryContactsTableForContacts(Set<Long> contactIds, long lastModified) {
    358     // Filter to after last modified time based only on contacts we care about
    359     String where =
    360         Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
    361             + " > ?"
    362             + " AND "
    363             + Contacts._ID
    364             + " IN ("
    365             + questionMarks(contactIds.size())
    366             + ")";
    367 
    368     String[] args = new String[contactIds.size() + 1];
    369     args[0] = Long.toString(lastModified);
    370     int i = 1;
    371     for (Long contactId : contactIds) {
    372       args[i++] = Long.toString(contactId);
    373     }
    374 
    375     return appContext
    376         .getContentResolver()
    377         .query(
    378             Contacts.CONTENT_URI,
    379             new String[] {Contacts._ID, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP},
    380             where,
    381             args,
    382             null);
    383   }
    384 
    385   private ListenableFuture<Boolean> noContactsModifiedSince(long lastModified) {
    386     return backgroundExecutorService.submit(
    387         () -> {
    388           try (Cursor cursor =
    389               appContext
    390                   .getContentResolver()
    391                   .query(
    392                       Contacts.CONTENT_URI,
    393                       new String[] {Contacts._ID},
    394                       Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?",
    395                       new String[] {Long.toString(lastModified)},
    396                       Contacts._ID + " limit 1")) {
    397             if (cursor == null) {
    398               LogUtil.w("Cp2DefaultDirectoryPhoneLookup.noContactsModifiedSince", "null cursor");
    399               return false;
    400             }
    401             return cursor.getCount() == 0;
    402           }
    403         });
    404   }
    405 
    406   /** Returns true if any contacts were deleted after {@code lastModified}. */
    407   private ListenableFuture<Boolean> anyContactsDeletedSince(long lastModified) {
    408     return backgroundExecutorService.submit(
    409         () -> {
    410           try (Cursor cursor =
    411               appContext
    412                   .getContentResolver()
    413                   .query(
    414                       DeletedContacts.CONTENT_URI,
    415                       new String[] {DeletedContacts.CONTACT_DELETED_TIMESTAMP},
    416                       DeletedContacts.CONTACT_DELETED_TIMESTAMP + " > ?",
    417                       new String[] {Long.toString(lastModified)},
    418                       DeletedContacts.CONTACT_DELETED_TIMESTAMP + " limit 1")) {
    419             if (cursor == null) {
    420               LogUtil.w("Cp2DefaultDirectoryPhoneLookup.anyContactsDeletedSince", "null cursor");
    421               return false;
    422             }
    423             return cursor.getCount() > 0;
    424           }
    425         });
    426   }
    427 
    428   @Override
    429   public void setSubMessage(PhoneLookupInfo.Builder destination, Cp2Info subMessage) {
    430     destination.setDefaultCp2Info(subMessage);
    431   }
    432 
    433   @Override
    434   public Cp2Info getSubMessage(PhoneLookupInfo phoneLookupInfo) {
    435     return phoneLookupInfo.getDefaultCp2Info();
    436   }
    437 
    438   @Override
    439   public ListenableFuture<ImmutableMap<DialerPhoneNumber, Cp2Info>> getMostRecentInfo(
    440       ImmutableMap<DialerPhoneNumber, Cp2Info> existingInfoMap) {
    441     currentLastTimestampProcessed = null;
    442 
    443     ListenableFuture<Long> lastModifiedFuture =
    444         backgroundExecutorService.submit(
    445             () -> sharedPreferences.getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L));
    446     return Futures.transformAsync(
    447         lastModifiedFuture,
    448         lastModified -> {
    449           // Build a set of each DialerPhoneNumber that was associated with a contact, and is no
    450           // longer associated with that same contact.
    451           ListenableFuture<Set<DialerPhoneNumber>> deletedPhoneNumbersFuture =
    452               getDeletedPhoneNumbers(existingInfoMap, lastModified);
    453 
    454           return Futures.transformAsync(
    455               deletedPhoneNumbersFuture,
    456               deletedPhoneNumbers -> {
    457 
    458                 // If there are too many invalid numbers, just defer the work to render time.
    459                 ArraySet<DialerPhoneNumber> unprocessableNumbers =
    460                     findUnprocessableNumbers(existingInfoMap);
    461                 Map<DialerPhoneNumber, Cp2Info> existingInfoMapToProcess = existingInfoMap;
    462                 if (!unprocessableNumbers.isEmpty()) {
    463                   existingInfoMapToProcess =
    464                       Maps.filterKeys(
    465                           existingInfoMap, number -> !unprocessableNumbers.contains(number));
    466                 }
    467 
    468                 // For each DialerPhoneNumber that was associated with a contact or added to a
    469                 // contact, build a map of those DialerPhoneNumbers to a set Cp2ContactInfos, where
    470                 // each Cp2ContactInfo represents a contact.
    471                 ListenableFuture<Map<DialerPhoneNumber, Set<Cp2ContactInfo>>>
    472                     updatedContactsFuture =
    473                         buildMapForUpdatedOrAddedContacts(
    474                             existingInfoMapToProcess, lastModified, deletedPhoneNumbers);
    475 
    476                 return Futures.transform(
    477                     updatedContactsFuture,
    478                     updatedContacts -> {
    479 
    480                       // Start build a new map of updated info. This will replace existing info.
    481                       ImmutableMap.Builder<DialerPhoneNumber, Cp2Info> newInfoMapBuilder =
    482                           ImmutableMap.builder();
    483 
    484                       // For each DialerPhoneNumber in existing info...
    485                       for (Entry<DialerPhoneNumber, Cp2Info> entry : existingInfoMap.entrySet()) {
    486                         DialerPhoneNumber dialerPhoneNumber = entry.getKey();
    487                         Cp2Info existingInfo = entry.getValue();
    488 
    489                         // Build off the existing info
    490                         Cp2Info.Builder infoBuilder = Cp2Info.newBuilder(existingInfo);
    491 
    492                         // If the contact was updated, replace the Cp2ContactInfo list
    493                         if (updatedContacts.containsKey(dialerPhoneNumber)) {
    494                           infoBuilder
    495                               .clear()
    496                               .addAllCp2ContactInfo(updatedContacts.get(dialerPhoneNumber));
    497                           // If it was deleted and not added to a new contact, clear all the CP2
    498                           // information.
    499                         } else if (deletedPhoneNumbers.contains(dialerPhoneNumber)) {
    500                           infoBuilder.clear();
    501                         } else if (unprocessableNumbers.contains(dialerPhoneNumber)) {
    502                           // Don't ever set the "incomplete" bit for numbers which are empty; this
    503                           // causes unnecessary render time work because there will never be contact
    504                           // information for an empty number. It is also required to pass the
    505                           // assertion check in the new voicemail fragment, which verifies that no
    506                           // voicemails rows are considered "incomplete" (the voicemail fragment
    507                           // does not have the ability to fetch information at render time).
    508                           if (!dialerPhoneNumber.getNormalizedNumber().isEmpty()) {
    509                             // Don't clear the existing info when the number is unprocessable. It's
    510                             // likely that the existing info is up-to-date so keep it in place so
    511                             // that the UI doesn't pop when the query is completed at display time.
    512                             infoBuilder.setIsIncomplete(true);
    513                           }
    514                         }
    515 
    516                         // If the DialerPhoneNumber didn't change, add the unchanged existing info.
    517                         newInfoMapBuilder.put(dialerPhoneNumber, infoBuilder.build());
    518                       }
    519                       return newInfoMapBuilder.build();
    520                     },
    521                     lightweightExecutorService);
    522               },
    523               lightweightExecutorService);
    524         },
    525         lightweightExecutorService);
    526   }
    527 
    528   private ArraySet<DialerPhoneNumber> findUnprocessableNumbers(
    529       ImmutableMap<DialerPhoneNumber, Cp2Info> existingInfoMap) {
    530     ArraySet<DialerPhoneNumber> unprocessableNumbers = new ArraySet<>();
    531     PartitionedNumbers partitionedNumbers = new PartitionedNumbers(existingInfoMap.keySet());
    532     if (partitionedNumbers.invalidNumbers().size() > MAX_SUPPORTED_INVALID_NUMBERS) {
    533       for (String invalidNumber : partitionedNumbers.invalidNumbers()) {
    534         unprocessableNumbers.addAll(partitionedNumbers.dialerPhoneNumbersForInvalid(invalidNumber));
    535       }
    536     }
    537     return unprocessableNumbers;
    538   }
    539 
    540   @Override
    541   public ListenableFuture<Void> onSuccessfulBulkUpdate() {
    542     return backgroundExecutorService.submit(
    543         () -> {
    544           if (currentLastTimestampProcessed != null) {
    545             sharedPreferences
    546                 .edit()
    547                 .putLong(PREF_LAST_TIMESTAMP_PROCESSED, currentLastTimestampProcessed)
    548                 .apply();
    549           }
    550           return null;
    551         });
    552   }
    553 
    554   private ListenableFuture<Set<DialerPhoneNumber>> findNumbersToUpdate(
    555       Map<DialerPhoneNumber, Cp2Info> existingInfoMap,
    556       long lastModified,
    557       Set<DialerPhoneNumber> deletedPhoneNumbers) {
    558     return backgroundExecutorService.submit(
    559         () -> {
    560           Set<DialerPhoneNumber> updatedNumbers = new ArraySet<>();
    561           Set<Long> contactIds = new ArraySet<>();
    562           for (Entry<DialerPhoneNumber, Cp2Info> entry : existingInfoMap.entrySet()) {
    563             DialerPhoneNumber dialerPhoneNumber = entry.getKey();
    564             Cp2Info existingInfo = entry.getValue();
    565 
    566             // If the number was deleted, we need to check if it was added to a new contact.
    567             if (deletedPhoneNumbers.contains(dialerPhoneNumber)) {
    568               updatedNumbers.add(dialerPhoneNumber);
    569               continue;
    570             }
    571 
    572             // When the PhoneLookupHistory contains no information for a number, because for
    573             // example the user just upgraded to the new UI, or cleared data, we need to check for
    574             // updated info.
    575             if (existingInfo.getCp2ContactInfoCount() == 0) {
    576               updatedNumbers.add(dialerPhoneNumber);
    577             } else {
    578               // For each Cp2ContactInfo for each existing DialerPhoneNumber...
    579               // Store the contact id if it exist, else automatically add the DialerPhoneNumber to
    580               // our set of DialerPhoneNumbers we want to update.
    581               for (Cp2ContactInfo cp2ContactInfo : existingInfo.getCp2ContactInfoList()) {
    582                 long existingContactId = cp2ContactInfo.getContactId();
    583                 if (existingContactId == 0) {
    584                   // If the number doesn't have a contact id, for various reasons, we need to look
    585                   // up the number to check if any exists. The various reasons this might happen
    586                   // are:
    587                   //  - An existing contact that wasn't in the call log is now in the call log.
    588                   //  - A number was in the call log before but has now been added to a contact.
    589                   //  - A number is in the call log, but isn't associated with any contact.
    590                   updatedNumbers.add(dialerPhoneNumber);
    591                 } else {
    592                   contactIds.add(cp2ContactInfo.getContactId());
    593                 }
    594               }
    595             }
    596           }
    597 
    598           // Query the contacts table and get those that whose
    599           // Contacts.CONTACT_LAST_UPDATED_TIMESTAMP is after lastModified, such that Contacts._ID
    600           // is in our set of contact IDs we build above.
    601           if (!contactIds.isEmpty()) {
    602             try (Cursor cursor = queryContactsTableForContacts(contactIds, lastModified)) {
    603               int contactIdIndex = cursor.getColumnIndex(Contacts._ID);
    604               int lastUpdatedIndex = cursor.getColumnIndex(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP);
    605               cursor.moveToPosition(-1);
    606               while (cursor.moveToNext()) {
    607                 // Find the DialerPhoneNumber for each contact id and add it to our updated numbers
    608                 // set. These, along with our number not associated with any Cp2ContactInfo need to
    609                 // be updated.
    610                 long contactId = cursor.getLong(contactIdIndex);
    611                 updatedNumbers.addAll(
    612                     findDialerPhoneNumbersContainingContactId(existingInfoMap, contactId));
    613                 long lastUpdatedTimestamp = cursor.getLong(lastUpdatedIndex);
    614                 if (currentLastTimestampProcessed == null
    615                     || currentLastTimestampProcessed < lastUpdatedTimestamp) {
    616                   currentLastTimestampProcessed = lastUpdatedTimestamp;
    617                 }
    618               }
    619             }
    620           }
    621           return updatedNumbers;
    622         });
    623   }
    624 
    625   @Override
    626   public void registerContentObservers(Context appContext) {
    627     // Do nothing since CP2 changes are too noisy.
    628   }
    629 
    630   /**
    631    * 1. get all contact ids. if the id is unset, add the number to the list of contacts to look up.
    632    * 2. reduce our list of contact ids to those that were updated after lastModified. 3. Now we have
    633    * the smallest set of dialer phone numbers to query cp2 against. 4. build and return the map of
    634    * dialerphonenumbers to their new Cp2ContactInfo
    635    *
    636    * @return Map of {@link DialerPhoneNumber} to {@link Cp2Info} with updated {@link
    637    *     Cp2ContactInfo}.
    638    */
    639   private ListenableFuture<Map<DialerPhoneNumber, Set<Cp2ContactInfo>>>
    640       buildMapForUpdatedOrAddedContacts(
    641           Map<DialerPhoneNumber, Cp2Info> existingInfoMap,
    642           long lastModified,
    643           Set<DialerPhoneNumber> deletedPhoneNumbers) {
    644     // Start by building a set of DialerPhoneNumbers that we want to update.
    645     ListenableFuture<Set<DialerPhoneNumber>> updatedNumbersFuture =
    646         findNumbersToUpdate(existingInfoMap, lastModified, deletedPhoneNumbers);
    647 
    648     return Futures.transformAsync(
    649         updatedNumbersFuture,
    650         updatedNumbers -> {
    651           if (updatedNumbers.isEmpty()) {
    652             return Futures.immediateFuture(new ArrayMap<>());
    653           }
    654 
    655           // Divide the numbers into those that are valid and those that are not. Issue a single
    656           // batch query for the valid numbers against the PHONE table, and in parallel issue
    657           // individual queries against PHONE_LOOKUP for each invalid number.
    658           // TODO(zachh): These queries are inefficient without a lastModified column to filter on.
    659           PartitionedNumbers partitionedNumbers =
    660               new PartitionedNumbers(ImmutableSet.copyOf(updatedNumbers));
    661 
    662           ListenableFuture<Map<String, Set<Cp2ContactInfo>>> validNumbersFuture =
    663               batchQueryForValidNumbers(partitionedNumbers.validE164Numbers());
    664 
    665           List<ListenableFuture<Set<Cp2ContactInfo>>> invalidNumbersFuturesList = new ArrayList<>();
    666           for (String invalidNumber : partitionedNumbers.invalidNumbers()) {
    667             invalidNumbersFuturesList.add(individualQueryForInvalidNumber(invalidNumber));
    668           }
    669 
    670           ListenableFuture<List<Set<Cp2ContactInfo>>> invalidNumbersFuture =
    671               Futures.allAsList(invalidNumbersFuturesList);
    672 
    673           Callable<Map<DialerPhoneNumber, Set<Cp2ContactInfo>>> computeMap =
    674               () -> {
    675                 // These get() calls are safe because we are using whenAllSucceed below.
    676                 Map<String, Set<Cp2ContactInfo>> validNumbersResult = validNumbersFuture.get();
    677                 List<Set<Cp2ContactInfo>> invalidNumbersResult = invalidNumbersFuture.get();
    678 
    679                 Map<DialerPhoneNumber, Set<Cp2ContactInfo>> map = new ArrayMap<>();
    680 
    681                 // First update the map with the valid number results.
    682                 for (Entry<String, Set<Cp2ContactInfo>> entry : validNumbersResult.entrySet()) {
    683                   String validNumber = entry.getKey();
    684                   Set<Cp2ContactInfo> cp2ContactInfos = entry.getValue();
    685 
    686                   Set<DialerPhoneNumber> dialerPhoneNumbers =
    687                       partitionedNumbers.dialerPhoneNumbersForValidE164(validNumber);
    688 
    689                   addInfo(map, dialerPhoneNumbers, cp2ContactInfos);
    690 
    691                   // We are going to remove the numbers that we've handled so that we later can
    692                   // detect numbers that weren't handled and therefore need to have their contact
    693                   // information removed.
    694                   updatedNumbers.removeAll(dialerPhoneNumbers);
    695                 }
    696 
    697                 // Next update the map with the invalid results.
    698                 int i = 0;
    699                 for (String invalidNumber : partitionedNumbers.invalidNumbers()) {
    700                   Set<Cp2ContactInfo> cp2Infos = invalidNumbersResult.get(i++);
    701                   Set<DialerPhoneNumber> dialerPhoneNumbers =
    702                       partitionedNumbers.dialerPhoneNumbersForInvalid(invalidNumber);
    703 
    704                   addInfo(map, dialerPhoneNumbers, cp2Infos);
    705 
    706                   // We are going to remove the numbers that we've handled so that we later can
    707                   // detect numbers that weren't handled and therefore need to have their contact
    708                   // information removed.
    709                   updatedNumbers.removeAll(dialerPhoneNumbers);
    710                 }
    711 
    712                 // The leftovers in updatedNumbers that weren't removed are numbers that were
    713                 // previously associated with contacts, but are no longer. Remove the contact
    714                 // information for them.
    715                 for (DialerPhoneNumber dialerPhoneNumber : updatedNumbers) {
    716                   map.put(dialerPhoneNumber, ImmutableSet.of());
    717                 }
    718                 LogUtil.v(
    719                     "Cp2DefaultDirectoryPhoneLookup.buildMapForUpdatedOrAddedContacts",
    720                     "found %d numbers that may need updating",
    721                     updatedNumbers.size());
    722                 return map;
    723               };
    724           return Futures.whenAllSucceed(validNumbersFuture, invalidNumbersFuture)
    725               .call(computeMap, lightweightExecutorService);
    726         },
    727         lightweightExecutorService);
    728   }
    729 
    730   private ListenableFuture<Map<String, Set<Cp2ContactInfo>>> batchQueryForValidNumbers(
    731       Set<String> validE164Numbers) {
    732     return backgroundExecutorService.submit(
    733         () -> {
    734           Map<String, Set<Cp2ContactInfo>> cp2ContactInfosByNumber = new ArrayMap<>();
    735           if (validE164Numbers.isEmpty()) {
    736             return cp2ContactInfosByNumber;
    737           }
    738           try (Cursor cursor =
    739               queryPhoneTableBasedOnE164(
    740                   Cp2Projections.getProjectionForPhoneTable(), validE164Numbers)) {
    741             if (cursor == null) {
    742               LogUtil.w("Cp2DefaultDirectoryPhoneLookup.batchQueryForValidNumbers", "null cursor");
    743             } else {
    744               while (cursor.moveToNext()) {
    745                 String validE164Number = Cp2Projections.getNormalizedNumberFromCursor(cursor);
    746                 Set<Cp2ContactInfo> cp2ContactInfos = cp2ContactInfosByNumber.get(validE164Number);
    747                 if (cp2ContactInfos == null) {
    748                   cp2ContactInfos = new ArraySet<>();
    749                   cp2ContactInfosByNumber.put(validE164Number, cp2ContactInfos);
    750                 }
    751                 cp2ContactInfos.add(
    752                     Cp2Projections.buildCp2ContactInfoFromCursor(appContext, cursor));
    753               }
    754             }
    755           }
    756           return cp2ContactInfosByNumber;
    757         });
    758   }
    759 
    760   private ListenableFuture<Set<Cp2ContactInfo>> individualQueryForInvalidNumber(
    761       String invalidNumber) {
    762     return backgroundExecutorService.submit(
    763         () -> {
    764           Set<Cp2ContactInfo> cp2ContactInfos = new ArraySet<>();
    765           if (invalidNumber.isEmpty()) {
    766             return cp2ContactInfos;
    767           }
    768           try (Cursor cursor =
    769               queryPhoneLookup(Cp2Projections.getProjectionForPhoneLookupTable(), invalidNumber)) {
    770             if (cursor == null) {
    771               LogUtil.w(
    772                   "Cp2DefaultDirectoryPhoneLookup.individualQueryForInvalidNumber", "null cursor");
    773             } else {
    774               while (cursor.moveToNext()) {
    775                 cp2ContactInfos.add(
    776                     Cp2Projections.buildCp2ContactInfoFromCursor(appContext, cursor));
    777               }
    778             }
    779           }
    780           return cp2ContactInfos;
    781         });
    782   }
    783 
    784   /**
    785    * Adds the {@code cp2ContactInfo} to the entries for all specified {@code dialerPhoneNumbers} in
    786    * the {@code map}.
    787    */
    788   private static void addInfo(
    789       Map<DialerPhoneNumber, Set<Cp2ContactInfo>> map,
    790       Set<DialerPhoneNumber> dialerPhoneNumbers,
    791       Set<Cp2ContactInfo> cp2ContactInfos) {
    792     for (DialerPhoneNumber dialerPhoneNumber : dialerPhoneNumbers) {
    793       Set<Cp2ContactInfo> existingInfos = map.get(dialerPhoneNumber);
    794       if (existingInfos == null) {
    795         existingInfos = new ArraySet<>();
    796         map.put(dialerPhoneNumber, existingInfos);
    797       }
    798       existingInfos.addAll(cp2ContactInfos);
    799     }
    800   }
    801 
    802   private Cursor queryPhoneTableBasedOnE164(String[] projection, Set<String> validE164Numbers) {
    803     return appContext
    804         .getContentResolver()
    805         .query(
    806             Phone.CONTENT_URI,
    807             projection,
    808             Phone.NORMALIZED_NUMBER + " IN (" + questionMarks(validE164Numbers.size()) + ")",
    809             validE164Numbers.toArray(new String[validE164Numbers.size()]),
    810             null);
    811   }
    812 
    813   private Cursor queryPhoneLookup(String[] projection, String rawNumber) {
    814     Uri uri =
    815         Uri.withAppendedPath(
    816             ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(rawNumber));
    817     return appContext.getContentResolver().query(uri, projection, null, null, null);
    818   }
    819 
    820   /** Returns set of DialerPhoneNumbers that were associated with now deleted contacts. */
    821   private ListenableFuture<Set<DialerPhoneNumber>> getDeletedPhoneNumbers(
    822       ImmutableMap<DialerPhoneNumber, Cp2Info> existingInfoMap, long lastModified) {
    823     return backgroundExecutorService.submit(
    824         () -> {
    825           // Build set of all contact IDs from our existing data. We're going to use this set to
    826           // query against the DeletedContacts table and see if any of them were deleted.
    827           Set<Long> contactIds = findContactIdsIn(existingInfoMap);
    828 
    829           // Start building a set of DialerPhoneNumbers that were associated with now deleted
    830           // contacts.
    831           try (Cursor cursor = queryDeletedContacts(contactIds, lastModified)) {
    832             // We now have a cursor/list of contact IDs that were associated with deleted contacts.
    833             return findDeletedPhoneNumbersIn(existingInfoMap, cursor);
    834           }
    835         });
    836   }
    837 
    838   private Set<Long> findContactIdsIn(ImmutableMap<DialerPhoneNumber, Cp2Info> map) {
    839     Set<Long> contactIds = new ArraySet<>();
    840     for (Cp2Info info : map.values()) {
    841       for (Cp2ContactInfo cp2ContactInfo : info.getCp2ContactInfoList()) {
    842         contactIds.add(cp2ContactInfo.getContactId());
    843       }
    844     }
    845     return contactIds;
    846   }
    847 
    848   private Cursor queryDeletedContacts(Set<Long> contactIds, long lastModified) {
    849     String where =
    850         DeletedContacts.CONTACT_DELETED_TIMESTAMP
    851             + " > ?"
    852             + " AND "
    853             + DeletedContacts.CONTACT_ID
    854             + " IN ("
    855             + questionMarks(contactIds.size())
    856             + ")";
    857     String[] args = new String[contactIds.size() + 1];
    858     args[0] = Long.toString(lastModified);
    859     int i = 1;
    860     for (Long contactId : contactIds) {
    861       args[i++] = Long.toString(contactId);
    862     }
    863 
    864     return appContext
    865         .getContentResolver()
    866         .query(
    867             DeletedContacts.CONTENT_URI,
    868             new String[] {DeletedContacts.CONTACT_ID, DeletedContacts.CONTACT_DELETED_TIMESTAMP},
    869             where,
    870             args,
    871             null);
    872   }
    873 
    874   /** Returns set of DialerPhoneNumbers that are associated with deleted contact IDs. */
    875   private Set<DialerPhoneNumber> findDeletedPhoneNumbersIn(
    876       ImmutableMap<DialerPhoneNumber, Cp2Info> existingInfoMap, Cursor cursor) {
    877     int contactIdIndex = cursor.getColumnIndexOrThrow(DeletedContacts.CONTACT_ID);
    878     int deletedTimeIndex = cursor.getColumnIndexOrThrow(DeletedContacts.CONTACT_DELETED_TIMESTAMP);
    879     Set<DialerPhoneNumber> deletedPhoneNumbers = new ArraySet<>();
    880     cursor.moveToPosition(-1);
    881     while (cursor.moveToNext()) {
    882       long contactId = cursor.getLong(contactIdIndex);
    883       deletedPhoneNumbers.addAll(
    884           findDialerPhoneNumbersContainingContactId(existingInfoMap, contactId));
    885       long deletedTime = cursor.getLong(deletedTimeIndex);
    886       if (currentLastTimestampProcessed == null || currentLastTimestampProcessed < deletedTime) {
    887         // TODO(zachh): There's a problem here if a contact for a new row is deleted?
    888         currentLastTimestampProcessed = deletedTime;
    889       }
    890     }
    891     return deletedPhoneNumbers;
    892   }
    893 
    894   private static Set<DialerPhoneNumber> findDialerPhoneNumbersContainingContactId(
    895       Map<DialerPhoneNumber, Cp2Info> existingInfoMap, long contactId) {
    896     Set<DialerPhoneNumber> matches = new ArraySet<>();
    897     for (Entry<DialerPhoneNumber, Cp2Info> entry : existingInfoMap.entrySet()) {
    898       for (Cp2ContactInfo cp2ContactInfo : entry.getValue().getCp2ContactInfoList()) {
    899         if (cp2ContactInfo.getContactId() == contactId) {
    900           matches.add(entry.getKey());
    901         }
    902       }
    903     }
    904     Assert.checkArgument(
    905         matches.size() > 0, "Couldn't find DialerPhoneNumber for contact ID: " + contactId);
    906     return matches;
    907   }
    908 
    909   private static String questionMarks(int count) {
    910     StringBuilder where = new StringBuilder();
    911     for (int i = 0; i < count; i++) {
    912       if (i != 0) {
    913         where.append(", ");
    914       }
    915       where.append("?");
    916     }
    917     return where.toString();
    918   }
    919 }
    920