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.calllog.datasources.phonelookup; 18 19 import android.content.ContentProviderOperation; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.content.OperationApplicationException; 23 import android.database.Cursor; 24 import android.os.RemoteException; 25 import android.support.annotation.MainThread; 26 import android.support.annotation.WorkerThread; 27 import android.text.TextUtils; 28 import android.util.ArrayMap; 29 import android.util.ArraySet; 30 import com.android.dialer.DialerPhoneNumber; 31 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; 32 import com.android.dialer.calllog.datasources.CallLogDataSource; 33 import com.android.dialer.calllog.datasources.CallLogMutations; 34 import com.android.dialer.calllogutils.NumberAttributesBuilder; 35 import com.android.dialer.common.Assert; 36 import com.android.dialer.common.LogUtil; 37 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; 38 import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; 39 import com.android.dialer.inject.ApplicationContext; 40 import com.android.dialer.phonelookup.PhoneLookup; 41 import com.android.dialer.phonelookup.PhoneLookupInfo; 42 import com.android.dialer.phonelookup.composite.CompositePhoneLookup; 43 import com.android.dialer.phonelookup.database.PhoneLookupHistoryDatabaseHelper; 44 import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract; 45 import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; 46 import com.google.common.collect.ImmutableMap; 47 import com.google.common.collect.ImmutableSet; 48 import com.google.common.collect.Maps; 49 import com.google.common.util.concurrent.Futures; 50 import com.google.common.util.concurrent.ListenableFuture; 51 import com.google.common.util.concurrent.ListeningExecutorService; 52 import com.google.common.util.concurrent.MoreExecutors; 53 import com.google.protobuf.InvalidProtocolBufferException; 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.Map; 57 import java.util.Map.Entry; 58 import java.util.Set; 59 import java.util.concurrent.Callable; 60 import javax.inject.Inject; 61 62 /** 63 * Responsible for maintaining the columns in the annotated call log which are derived from phone 64 * numbers. 65 */ 66 public final class PhoneLookupDataSource implements CallLogDataSource { 67 68 private final Context appContext; 69 private final CompositePhoneLookup compositePhoneLookup; 70 private final ListeningExecutorService backgroundExecutorService; 71 private final ListeningExecutorService lightweightExecutorService; 72 73 /** 74 * Keyed by normalized number (the primary key for PhoneLookupHistory). 75 * 76 * <p>This is state saved between the {@link CallLogDataSource#fill(CallLogMutations)} and {@link 77 * CallLogDataSource#onSuccessfulFill()} operations. 78 */ 79 private final Map<String, PhoneLookupInfo> phoneLookupHistoryRowsToUpdate = new ArrayMap<>(); 80 81 /** 82 * Normalized numbers (the primary key for PhoneLookupHistory) which should be deleted from 83 * PhoneLookupHistory. 84 * 85 * <p>This is state saved between the {@link CallLogDataSource#fill(CallLogMutations)} and {@link 86 * CallLogDataSource#onSuccessfulFill()} operations. 87 */ 88 private final Set<String> phoneLookupHistoryRowsToDelete = new ArraySet<>(); 89 90 private final PhoneLookupHistoryDatabaseHelper phoneLookupHistoryDatabaseHelper; 91 92 @Inject PhoneLookupDataSource( @pplicationContext Context appContext, CompositePhoneLookup compositePhoneLookup, @BackgroundExecutor ListeningExecutorService backgroundExecutorService, @LightweightExecutor ListeningExecutorService lightweightExecutorService, PhoneLookupHistoryDatabaseHelper phoneLookupHistoryDatabaseHelper)93 PhoneLookupDataSource( 94 @ApplicationContext Context appContext, 95 CompositePhoneLookup compositePhoneLookup, 96 @BackgroundExecutor ListeningExecutorService backgroundExecutorService, 97 @LightweightExecutor ListeningExecutorService lightweightExecutorService, 98 PhoneLookupHistoryDatabaseHelper phoneLookupHistoryDatabaseHelper) { 99 this.appContext = appContext; 100 this.compositePhoneLookup = compositePhoneLookup; 101 this.backgroundExecutorService = backgroundExecutorService; 102 this.lightweightExecutorService = lightweightExecutorService; 103 this.phoneLookupHistoryDatabaseHelper = phoneLookupHistoryDatabaseHelper; 104 } 105 106 @Override isDirty()107 public ListenableFuture<Boolean> isDirty() { 108 ListenableFuture<ImmutableSet<DialerPhoneNumber>> phoneNumbers = 109 backgroundExecutorService.submit( 110 () -> queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(appContext)); 111 return Futures.transformAsync( 112 phoneNumbers, compositePhoneLookup::isDirty, lightweightExecutorService); 113 } 114 115 /** 116 * {@inheritDoc} 117 * 118 * <p>This method uses the following algorithm: 119 * 120 * <ul> 121 * <li>Finds the phone numbers of interest by taking the union of the distinct 122 * DialerPhoneNumbers from the AnnotatedCallLog and the pending inserts provided in {@code 123 * mutations} 124 * <li>Uses them to fetch the current information from PhoneLookupHistory, in order to construct 125 * a map from DialerPhoneNumber to PhoneLookupInfo 126 * <ul> 127 * <li>If no PhoneLookupInfo is found (e.g. app data was cleared?) an empty value is used. 128 * </ul> 129 * <li>Looks through the provided set of mutations 130 * <li>For inserts, uses the contents of PhoneLookupHistory to populate the fields of the 131 * provided mutations. (Note that at this point, data may not be fully up-to-date, but the 132 * next steps will take care of that.) 133 * <li>Uses all of the numbers from AnnotatedCallLog to invoke (composite) {@link 134 * PhoneLookup#getMostRecentInfo(ImmutableMap)} 135 * <li>Looks through the results of getMostRecentInfo 136 * <ul> 137 * <li>For each number, checks if the original PhoneLookupInfo differs from the new one 138 * <li>If so, it applies the update to the mutations and (in onSuccessfulFill) writes the 139 * new value back to the PhoneLookupHistory. 140 * </ul> 141 * </ul> 142 */ 143 @Override fill(CallLogMutations mutations)144 public ListenableFuture<Void> fill(CallLogMutations mutations) { 145 LogUtil.v( 146 "PhoneLookupDataSource.fill", 147 "processing mutations (inserts: %d, updates: %d, deletes: %d)", 148 mutations.getInserts().size(), 149 mutations.getUpdates().size(), 150 mutations.getDeletes().size()); 151 152 // Clear state saved since the last call to fill. This is necessary in case fill is called but 153 // onSuccessfulFill is not called during a previous flow. 154 phoneLookupHistoryRowsToUpdate.clear(); 155 phoneLookupHistoryRowsToDelete.clear(); 156 157 // First query information from annotated call log (and include pending inserts). 158 ListenableFuture<Map<DialerPhoneNumber, Set<Long>>> annotatedCallLogIdsByNumberFuture = 159 backgroundExecutorService.submit( 160 () -> collectIdAndNumberFromAnnotatedCallLogAndPendingInserts(appContext, mutations)); 161 162 // Use it to create the original info map. 163 ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> originalInfoMapFuture = 164 Futures.transform( 165 annotatedCallLogIdsByNumberFuture, 166 annotatedCallLogIdsByNumber -> 167 queryPhoneLookupHistoryForNumbers(appContext, annotatedCallLogIdsByNumber.keySet()), 168 backgroundExecutorService); 169 170 // Use the original info map to generate the updated info map by delegating to 171 // compositePhoneLookup. 172 ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> updatedInfoMapFuture = 173 Futures.transformAsync( 174 originalInfoMapFuture, 175 compositePhoneLookup::getMostRecentInfo, 176 lightweightExecutorService); 177 178 // This is the computation that will use the result of all of the above. 179 Callable<ImmutableMap<Long, PhoneLookupInfo>> computeRowsToUpdate = 180 () -> { 181 // These get() calls are safe because we are using whenAllSucceed below. 182 Map<DialerPhoneNumber, Set<Long>> annotatedCallLogIdsByNumber = 183 annotatedCallLogIdsByNumberFuture.get(); 184 ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> originalInfoMap = 185 originalInfoMapFuture.get(); 186 ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> updatedInfoMap = 187 updatedInfoMapFuture.get(); 188 189 // First populate the insert mutations 190 ImmutableMap.Builder<Long, PhoneLookupInfo> 191 originalPhoneLookupHistoryDataByAnnotatedCallLogId = ImmutableMap.builder(); 192 for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : originalInfoMap.entrySet()) { 193 DialerPhoneNumber dialerPhoneNumber = entry.getKey(); 194 PhoneLookupInfo phoneLookupInfo = entry.getValue(); 195 for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { 196 originalPhoneLookupHistoryDataByAnnotatedCallLogId.put(id, phoneLookupInfo); 197 } 198 } 199 populateInserts(originalPhoneLookupHistoryDataByAnnotatedCallLogId.build(), mutations); 200 201 // Compute and save the PhoneLookupHistory rows which can be deleted in onSuccessfulFill. 202 phoneLookupHistoryRowsToDelete.addAll( 203 computePhoneLookupHistoryRowsToDelete(annotatedCallLogIdsByNumber, mutations)); 204 205 // Now compute the rows to update. 206 ImmutableMap.Builder<Long, PhoneLookupInfo> rowsToUpdate = ImmutableMap.builder(); 207 for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : updatedInfoMap.entrySet()) { 208 DialerPhoneNumber dialerPhoneNumber = entry.getKey(); 209 PhoneLookupInfo upToDateInfo = entry.getValue(); 210 if (!originalInfoMap.get(dialerPhoneNumber).equals(upToDateInfo)) { 211 for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { 212 rowsToUpdate.put(id, upToDateInfo); 213 } 214 // Also save the updated information so that it can be written to PhoneLookupHistory 215 // in onSuccessfulFill. 216 // Note: This loses country info when number is not valid. 217 String normalizedNumber = dialerPhoneNumber.getNormalizedNumber(); 218 phoneLookupHistoryRowsToUpdate.put(normalizedNumber, upToDateInfo); 219 } 220 } 221 return rowsToUpdate.build(); 222 }; 223 224 ListenableFuture<ImmutableMap<Long, PhoneLookupInfo>> rowsToUpdateFuture = 225 Futures.whenAllSucceed( 226 annotatedCallLogIdsByNumberFuture, updatedInfoMapFuture, originalInfoMapFuture) 227 .call( 228 computeRowsToUpdate, 229 backgroundExecutorService /* PhoneNumberUtil may do disk IO */); 230 231 // Finally update the mutations with the computed rows. 232 return Futures.transform( 233 rowsToUpdateFuture, 234 rowsToUpdate -> { 235 updateMutations(rowsToUpdate, mutations); 236 LogUtil.v( 237 "PhoneLookupDataSource.fill", 238 "updated mutations (inserts: %d, updates: %d, deletes: %d)", 239 mutations.getInserts().size(), 240 mutations.getUpdates().size(), 241 mutations.getDeletes().size()); 242 return null; 243 }, 244 lightweightExecutorService); 245 } 246 247 @Override onSuccessfulFill()248 public ListenableFuture<Void> onSuccessfulFill() { 249 // First update and/or delete the appropriate rows in PhoneLookupHistory. 250 ListenableFuture<Void> writePhoneLookupHistory = 251 backgroundExecutorService.submit(() -> writePhoneLookupHistory(appContext)); 252 253 // If that succeeds, delegate to the composite PhoneLookup to notify all PhoneLookups that both 254 // the AnnotatedCallLog and PhoneLookupHistory have been successfully updated. 255 return Futures.transformAsync( 256 writePhoneLookupHistory, 257 unused -> compositePhoneLookup.onSuccessfulBulkUpdate(), 258 lightweightExecutorService); 259 } 260 261 @WorkerThread writePhoneLookupHistory(Context appContext)262 private Void writePhoneLookupHistory(Context appContext) 263 throws RemoteException, OperationApplicationException { 264 ArrayList<ContentProviderOperation> operations = new ArrayList<>(); 265 long currentTimestamp = System.currentTimeMillis(); 266 for (Entry<String, PhoneLookupInfo> entry : phoneLookupHistoryRowsToUpdate.entrySet()) { 267 String normalizedNumber = entry.getKey(); 268 PhoneLookupInfo phoneLookupInfo = entry.getValue(); 269 ContentValues contentValues = new ContentValues(); 270 contentValues.put(PhoneLookupHistory.PHONE_LOOKUP_INFO, phoneLookupInfo.toByteArray()); 271 contentValues.put(PhoneLookupHistory.LAST_MODIFIED, currentTimestamp); 272 operations.add( 273 ContentProviderOperation.newUpdate( 274 PhoneLookupHistory.contentUriForNumber(normalizedNumber)) 275 .withValues(contentValues) 276 .build()); 277 } 278 for (String normalizedNumber : phoneLookupHistoryRowsToDelete) { 279 operations.add( 280 ContentProviderOperation.newDelete( 281 PhoneLookupHistory.contentUriForNumber(normalizedNumber)) 282 .build()); 283 } 284 Assert.isNotNull( 285 appContext 286 .getContentResolver() 287 .applyBatch(PhoneLookupHistoryContract.AUTHORITY, operations)); 288 return null; 289 } 290 291 @MainThread 292 @Override registerContentObservers()293 public void registerContentObservers() { 294 compositePhoneLookup.registerContentObservers(); 295 } 296 297 @Override unregisterContentObservers()298 public void unregisterContentObservers() { 299 compositePhoneLookup.unregisterContentObservers(); 300 } 301 302 @Override clearData()303 public ListenableFuture<Void> clearData() { 304 ListenableFuture<Void> clearDataFuture = compositePhoneLookup.clearData(); 305 ListenableFuture<Void> deleteDatabaseFuture = phoneLookupHistoryDatabaseHelper.delete(); 306 307 return Futures.transform( 308 Futures.allAsList(clearDataFuture, deleteDatabaseFuture), 309 unused -> null, 310 MoreExecutors.directExecutor()); 311 } 312 313 @Override getLoggingName()314 public String getLoggingName() { 315 return "PhoneLookupDataSource"; 316 } 317 318 private static ImmutableSet<DialerPhoneNumber> queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(Context appContext)319 queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(Context appContext) { 320 ImmutableSet.Builder<DialerPhoneNumber> numbers = ImmutableSet.builder(); 321 322 try (Cursor cursor = 323 appContext 324 .getContentResolver() 325 .query( 326 AnnotatedCallLog.DISTINCT_NUMBERS_CONTENT_URI, 327 new String[] {AnnotatedCallLog.NUMBER}, 328 null, 329 null, 330 null)) { 331 332 if (cursor == null) { 333 LogUtil.e( 334 "PhoneLookupDataSource.queryDistinctDialerPhoneNumbersFromAnnotatedCallLog", 335 "null cursor"); 336 return numbers.build(); 337 } 338 339 if (cursor.moveToFirst()) { 340 int numberColumn = cursor.getColumnIndexOrThrow(AnnotatedCallLog.NUMBER); 341 do { 342 byte[] blob = cursor.getBlob(numberColumn); 343 if (blob == null) { 344 // Not all [incoming] calls have associated phone numbers. 345 continue; 346 } 347 try { 348 numbers.add(DialerPhoneNumber.parseFrom(blob)); 349 } catch (InvalidProtocolBufferException e) { 350 throw new IllegalStateException(e); 351 } 352 } while (cursor.moveToNext()); 353 } 354 } 355 return numbers.build(); 356 } 357 collectIdAndNumberFromAnnotatedCallLogAndPendingInserts( Context appContext, CallLogMutations mutations)358 private Map<DialerPhoneNumber, Set<Long>> collectIdAndNumberFromAnnotatedCallLogAndPendingInserts( 359 Context appContext, CallLogMutations mutations) { 360 Map<DialerPhoneNumber, Set<Long>> idsByNumber = new ArrayMap<>(); 361 // First add any pending inserts to the map. 362 for (Entry<Long, ContentValues> entry : mutations.getInserts().entrySet()) { 363 long id = entry.getKey(); 364 ContentValues insertedContentValues = entry.getValue(); 365 DialerPhoneNumber dialerPhoneNumber; 366 try { 367 dialerPhoneNumber = 368 DialerPhoneNumber.parseFrom( 369 insertedContentValues.getAsByteArray(AnnotatedCallLog.NUMBER)); 370 } catch (InvalidProtocolBufferException e) { 371 throw new IllegalStateException(e); 372 } 373 Set<Long> ids = idsByNumber.get(dialerPhoneNumber); 374 if (ids == null) { 375 ids = new ArraySet<>(); 376 idsByNumber.put(dialerPhoneNumber, ids); 377 } 378 ids.add(id); 379 } 380 381 try (Cursor cursor = 382 appContext 383 .getContentResolver() 384 .query( 385 AnnotatedCallLog.CONTENT_URI, 386 new String[] {AnnotatedCallLog._ID, AnnotatedCallLog.NUMBER}, 387 null, 388 null, 389 null)) { 390 391 if (cursor == null) { 392 LogUtil.e( 393 "PhoneLookupDataSource.collectIdAndNumberFromAnnotatedCallLogAndPendingInserts", 394 "null cursor"); 395 return ImmutableMap.of(); 396 } 397 398 if (cursor.moveToFirst()) { 399 int idColumn = cursor.getColumnIndexOrThrow(AnnotatedCallLog._ID); 400 int numberColumn = cursor.getColumnIndexOrThrow(AnnotatedCallLog.NUMBER); 401 do { 402 long id = cursor.getLong(idColumn); 403 byte[] blob = cursor.getBlob(numberColumn); 404 if (blob == null) { 405 // Not all [incoming] calls have associated phone numbers. 406 continue; 407 } 408 DialerPhoneNumber dialerPhoneNumber; 409 try { 410 dialerPhoneNumber = DialerPhoneNumber.parseFrom(blob); 411 } catch (InvalidProtocolBufferException e) { 412 throw new IllegalStateException(e); 413 } 414 Set<Long> ids = idsByNumber.get(dialerPhoneNumber); 415 if (ids == null) { 416 ids = new ArraySet<>(); 417 idsByNumber.put(dialerPhoneNumber, ids); 418 } 419 ids.add(id); 420 } while (cursor.moveToNext()); 421 } 422 } 423 return idsByNumber; 424 } 425 426 /** Returned map must have same keys as {@code uniqueDialerPhoneNumbers} */ queryPhoneLookupHistoryForNumbers( Context appContext, Set<DialerPhoneNumber> uniqueDialerPhoneNumbers)427 private ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> queryPhoneLookupHistoryForNumbers( 428 Context appContext, Set<DialerPhoneNumber> uniqueDialerPhoneNumbers) { 429 // Note: This loses country info when number is not valid. 430 Map<DialerPhoneNumber, String> dialerPhoneNumberToNormalizedNumbers = 431 Maps.asMap(uniqueDialerPhoneNumbers, DialerPhoneNumber::getNormalizedNumber); 432 433 // Convert values to a set to remove any duplicates that are the result of two 434 // DialerPhoneNumbers mapping to the same normalized number. 435 String[] normalizedNumbers = 436 dialerPhoneNumberToNormalizedNumbers.values().toArray(new String[] {}); 437 String[] questionMarks = new String[normalizedNumbers.length]; 438 Arrays.fill(questionMarks, "?"); 439 String selection = 440 PhoneLookupHistory.NORMALIZED_NUMBER + " in (" + TextUtils.join(",", questionMarks) + ")"; 441 442 Map<String, PhoneLookupInfo> normalizedNumberToInfoMap = new ArrayMap<>(); 443 try (Cursor cursor = 444 appContext 445 .getContentResolver() 446 .query( 447 PhoneLookupHistory.CONTENT_URI, 448 new String[] { 449 PhoneLookupHistory.NORMALIZED_NUMBER, PhoneLookupHistory.PHONE_LOOKUP_INFO, 450 }, 451 selection, 452 normalizedNumbers, 453 null)) { 454 if (cursor == null) { 455 LogUtil.e("PhoneLookupDataSource.queryPhoneLookupHistoryForNumbers", "null cursor"); 456 } else if (cursor.moveToFirst()) { 457 int normalizedNumberColumn = 458 cursor.getColumnIndexOrThrow(PhoneLookupHistory.NORMALIZED_NUMBER); 459 int phoneLookupInfoColumn = 460 cursor.getColumnIndexOrThrow(PhoneLookupHistory.PHONE_LOOKUP_INFO); 461 do { 462 String normalizedNumber = cursor.getString(normalizedNumberColumn); 463 PhoneLookupInfo phoneLookupInfo; 464 try { 465 phoneLookupInfo = PhoneLookupInfo.parseFrom(cursor.getBlob(phoneLookupInfoColumn)); 466 } catch (InvalidProtocolBufferException e) { 467 throw new IllegalStateException(e); 468 } 469 normalizedNumberToInfoMap.put(normalizedNumber, phoneLookupInfo); 470 } while (cursor.moveToNext()); 471 } 472 } 473 474 // We have the required information in normalizedNumberToInfoMap but it's keyed by normalized 475 // number instead of DialerPhoneNumber. Build and return a new map keyed by DialerPhoneNumber. 476 return ImmutableMap.copyOf( 477 Maps.asMap( 478 uniqueDialerPhoneNumbers, 479 (dialerPhoneNumber) -> { 480 String normalizedNumber = dialerPhoneNumberToNormalizedNumbers.get(dialerPhoneNumber); 481 PhoneLookupInfo phoneLookupInfo = normalizedNumberToInfoMap.get(normalizedNumber); 482 // If data is cleared or for other reasons, the PhoneLookupHistory may not contain an 483 // entry for a number. Just use an empty value for that case. 484 return phoneLookupInfo == null 485 ? PhoneLookupInfo.getDefaultInstance() 486 : phoneLookupInfo; 487 })); 488 } 489 490 private void populateInserts( 491 ImmutableMap<Long, PhoneLookupInfo> existingInfo, CallLogMutations mutations) { 492 for (Entry<Long, ContentValues> entry : mutations.getInserts().entrySet()) { 493 long id = entry.getKey(); 494 ContentValues contentValues = entry.getValue(); 495 PhoneLookupInfo phoneLookupInfo = existingInfo.get(id); 496 // Existing info might be missing if data was cleared or for other reasons. 497 if (phoneLookupInfo != null) { 498 updateContentValues(contentValues, phoneLookupInfo); 499 } 500 } 501 } 502 503 private void updateMutations( 504 ImmutableMap<Long, PhoneLookupInfo> updatesToApply, CallLogMutations mutations) { 505 for (Entry<Long, PhoneLookupInfo> entry : updatesToApply.entrySet()) { 506 long id = entry.getKey(); 507 PhoneLookupInfo phoneLookupInfo = entry.getValue(); 508 ContentValues contentValuesToInsert = mutations.getInserts().get(id); 509 if (contentValuesToInsert != null) { 510 /* 511 * This is a confusing case. Consider: 512 * 513 * 1) An incoming call from "Bob" arrives; "Bob" is written to PhoneLookupHistory. 514 * 2) User changes Bob's name to "Robert". 515 * 3) User opens call log, and this code is invoked with the inserted call as a mutation. 516 * 517 * In populateInserts, we retrieved "Bob" from PhoneLookupHistory and wrote it to the insert 518 * mutation, which is wrong. We need to actually ask the phone lookups for the most up to 519 * date information ("Robert"), and update the "insert" mutation again. 520 * 521 * Having understood this, you may wonder why populateInserts() is needed at all--excellent 522 * question! Consider: 523 * 524 * 1) An incoming call from number 123 ("Bob") arrives at time T1; "Bob" is written to 525 * PhoneLookupHistory. 526 * 2) User opens call log at time T2 and "Bob" is written to it, and everything is fine; the 527 * call log can be considered accurate as of T2. 528 * 3) An incoming call from number 456 ("John") arrives at time T3. Let's say the contact 529 * info for John was last modified at time T0. 530 * 4) Now imagine that populateInserts() didn't exist; the phone lookup will ask for any 531 * information for phone number 456 which has changed since T2--but "John" hasn't changed 532 * since then so no contact information would be found. 533 * 534 * The populateInserts() method avoids this problem by always first populating inserted 535 * mutations from PhoneLookupHistory; in this case "John" would be copied during 536 * populateInserts() and there wouldn't be further updates needed here. 537 */ 538 updateContentValues(contentValuesToInsert, phoneLookupInfo); 539 continue; 540 } 541 ContentValues contentValuesToUpdate = mutations.getUpdates().get(id); 542 if (contentValuesToUpdate != null) { 543 updateContentValues(contentValuesToUpdate, phoneLookupInfo); 544 continue; 545 } 546 // Else this row is not already scheduled for insert or update and we need to schedule it. 547 ContentValues contentValues = new ContentValues(); 548 updateContentValues(contentValues, phoneLookupInfo); 549 mutations.getUpdates().put(id, contentValues); 550 } 551 } 552 553 private Set<String> computePhoneLookupHistoryRowsToDelete( 554 Map<DialerPhoneNumber, Set<Long>> annotatedCallLogIdsByNumber, CallLogMutations mutations) { 555 if (mutations.getDeletes().isEmpty()) { 556 return ImmutableSet.of(); 557 } 558 // First convert the dialer phone numbers to normalized numbers; we need to combine entries 559 // because different DialerPhoneNumbers can map to the same normalized number. 560 Map<String, Set<Long>> idsByNormalizedNumber = new ArrayMap<>(); 561 for (Entry<DialerPhoneNumber, Set<Long>> entry : annotatedCallLogIdsByNumber.entrySet()) { 562 DialerPhoneNumber dialerPhoneNumber = entry.getKey(); 563 Set<Long> idsForDialerPhoneNumber = entry.getValue(); 564 // Note: This loses country info when number is not valid. 565 String normalizedNumber = dialerPhoneNumber.getNormalizedNumber(); 566 Set<Long> idsForNormalizedNumber = idsByNormalizedNumber.get(normalizedNumber); 567 if (idsForNormalizedNumber == null) { 568 idsForNormalizedNumber = new ArraySet<>(); 569 idsByNormalizedNumber.put(normalizedNumber, idsForNormalizedNumber); 570 } 571 idsForNormalizedNumber.addAll(idsForDialerPhoneNumber); 572 } 573 // Now look through and remove all IDs that were scheduled for delete; after doing that, if 574 // there are no remaining IDs left for a normalized number, the number can be deleted from 575 // PhoneLookupHistory. 576 Set<String> normalizedNumbersToDelete = new ArraySet<>(); 577 for (Entry<String, Set<Long>> entry : idsByNormalizedNumber.entrySet()) { 578 String normalizedNumber = entry.getKey(); 579 Set<Long> idsForNormalizedNumber = entry.getValue(); 580 idsForNormalizedNumber.removeAll(mutations.getDeletes()); 581 if (idsForNormalizedNumber.isEmpty()) { 582 normalizedNumbersToDelete.add(normalizedNumber); 583 } 584 } 585 return normalizedNumbersToDelete; 586 } 587 588 private void updateContentValues(ContentValues contentValues, PhoneLookupInfo phoneLookupInfo) { 589 contentValues.put( 590 AnnotatedCallLog.NUMBER_ATTRIBUTES, 591 NumberAttributesBuilder.fromPhoneLookupInfo(phoneLookupInfo).build().toByteArray()); 592 } 593 } 594