Home | History | Annotate | Download | only in phonelookup
      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