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