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