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