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