• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
PhoneLookupDataSource( CompositePhoneLookup compositePhoneLookup, @BackgroundExecutor ListeningExecutorService backgroundExecutorService, @LightweightExecutor ListeningExecutorService lightweightExecutorService)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
isDirty(Context appContext)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
fill(Context appContext, CallLogMutations mutations)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
onSuccessfulFill(Context appContext)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
writePhoneLookupHistory(Context appContext)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
coalesce(List<ContentValues> individualRowsSortedByTimestampDesc)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
registerContentObservers(Context appContext)293   public void registerContentObservers(Context appContext) {
294     compositePhoneLookup.registerContentObservers(appContext);
295   }
296 
297   private static ImmutableSet<DialerPhoneNumber>
queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(Context appContext)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 
collectIdAndNumberFromAnnotatedCallLogAndPendingInserts( Context appContext, CallLogMutations mutations)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} */
queryPhoneLookupHistoryForNumbers( Context appContext, Set<DialerPhoneNumber> 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