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
17package com.android.dialer.calllog.datasources.phonelookup;
18
19import android.content.ContentProviderOperation;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.OperationApplicationException;
23import android.database.Cursor;
24import android.os.RemoteException;
25import android.support.annotation.MainThread;
26import android.support.annotation.WorkerThread;
27import android.text.TextUtils;
28import android.util.ArrayMap;
29import android.util.ArraySet;
30import com.android.dialer.DialerPhoneNumber;
31import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
32import com.android.dialer.calllog.datasources.CallLogDataSource;
33import com.android.dialer.calllog.datasources.CallLogMutations;
34import com.android.dialer.calllog.datasources.util.RowCombiner;
35import com.android.dialer.calllogutils.NumberAttributesConverter;
36import com.android.dialer.common.Assert;
37import com.android.dialer.common.LogUtil;
38import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
39import com.android.dialer.common.concurrent.Annotations.LightweightExecutor;
40import com.android.dialer.phonelookup.PhoneLookup;
41import com.android.dialer.phonelookup.PhoneLookupInfo;
42import com.android.dialer.phonelookup.composite.CompositePhoneLookup;
43import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract;
44import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory;
45import com.google.common.collect.ImmutableMap;
46import com.google.common.collect.ImmutableSet;
47import com.google.common.collect.Maps;
48import com.google.common.util.concurrent.Futures;
49import com.google.common.util.concurrent.ListenableFuture;
50import com.google.common.util.concurrent.ListeningExecutorService;
51import com.google.protobuf.InvalidProtocolBufferException;
52import java.util.ArrayList;
53import java.util.Arrays;
54import java.util.List;
55import java.util.Map;
56import java.util.Map.Entry;
57import java.util.Set;
58import java.util.concurrent.Callable;
59import javax.inject.Inject;
60
61/**
62 * Responsible for maintaining the columns in the annotated call log which are derived from phone
63 * numbers.
64 */
65public 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