/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.dialer.calllog.datasources.phonelookup;

import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.os.RemoteException;
import android.support.annotation.MainThread;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import com.android.dialer.DialerPhoneNumber;
import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
import com.android.dialer.calllog.datasources.CallLogDataSource;
import com.android.dialer.calllog.datasources.CallLogMutations;
import com.android.dialer.calllogutils.NumberAttributesBuilder;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
import com.android.dialer.common.concurrent.Annotations.LightweightExecutor;
import com.android.dialer.inject.ApplicationContext;
import com.android.dialer.phonelookup.PhoneLookup;
import com.android.dialer.phonelookup.PhoneLookupInfo;
import com.android.dialer.phonelookup.composite.CompositePhoneLookup;
import com.android.dialer.phonelookup.database.PhoneLookupHistoryDatabaseHelper;
import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract;
import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.InvalidProtocolBufferException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import javax.inject.Inject;

/**
 * Responsible for maintaining the columns in the annotated call log which are derived from phone
 * numbers.
 */
public final class PhoneLookupDataSource implements CallLogDataSource {

  private final Context appContext;
  private final CompositePhoneLookup compositePhoneLookup;
  private final ListeningExecutorService backgroundExecutorService;
  private final ListeningExecutorService lightweightExecutorService;

  /**
   * Keyed by normalized number (the primary key for PhoneLookupHistory).
   *
   * <p>This is state saved between the {@link CallLogDataSource#fill(CallLogMutations)} and {@link
   * CallLogDataSource#onSuccessfulFill()} operations.
   */
  private final Map<String, PhoneLookupInfo> phoneLookupHistoryRowsToUpdate = new ArrayMap<>();

  /**
   * Normalized numbers (the primary key for PhoneLookupHistory) which should be deleted from
   * PhoneLookupHistory.
   *
   * <p>This is state saved between the {@link CallLogDataSource#fill(CallLogMutations)} and {@link
   * CallLogDataSource#onSuccessfulFill()} operations.
   */
  private final Set<String> phoneLookupHistoryRowsToDelete = new ArraySet<>();

  private final PhoneLookupHistoryDatabaseHelper phoneLookupHistoryDatabaseHelper;

  @Inject
  PhoneLookupDataSource(
      @ApplicationContext Context appContext,
      CompositePhoneLookup compositePhoneLookup,
      @BackgroundExecutor ListeningExecutorService backgroundExecutorService,
      @LightweightExecutor ListeningExecutorService lightweightExecutorService,
      PhoneLookupHistoryDatabaseHelper phoneLookupHistoryDatabaseHelper) {
    this.appContext = appContext;
    this.compositePhoneLookup = compositePhoneLookup;
    this.backgroundExecutorService = backgroundExecutorService;
    this.lightweightExecutorService = lightweightExecutorService;
    this.phoneLookupHistoryDatabaseHelper = phoneLookupHistoryDatabaseHelper;
  }

  @Override
  public ListenableFuture<Boolean> isDirty() {
    ListenableFuture<ImmutableSet<DialerPhoneNumber>> phoneNumbers =
        backgroundExecutorService.submit(
            () -> queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(appContext));
    return Futures.transformAsync(
        phoneNumbers, compositePhoneLookup::isDirty, lightweightExecutorService);
  }

  /**
   * {@inheritDoc}
   *
   * <p>This method uses the following algorithm:
   *
   * <ul>
   *   <li>Finds the phone numbers of interest by taking the union of the distinct
   *       DialerPhoneNumbers from the AnnotatedCallLog and the pending inserts provided in {@code
   *       mutations}
   *   <li>Uses them to fetch the current information from PhoneLookupHistory, in order to construct
   *       a map from DialerPhoneNumber to PhoneLookupInfo
   *       <ul>
   *         <li>If no PhoneLookupInfo is found (e.g. app data was cleared?) an empty value is used.
   *       </ul>
   *   <li>Looks through the provided set of mutations
   *   <li>For inserts, uses the contents of PhoneLookupHistory to populate the fields of the
   *       provided mutations. (Note that at this point, data may not be fully up-to-date, but the
   *       next steps will take care of that.)
   *   <li>Uses all of the numbers from AnnotatedCallLog to invoke (composite) {@link
   *       PhoneLookup#getMostRecentInfo(ImmutableMap)}
   *   <li>Looks through the results of getMostRecentInfo
   *       <ul>
   *         <li>For each number, checks if the original PhoneLookupInfo differs from the new one
   *         <li>If so, it applies the update to the mutations and (in onSuccessfulFill) writes the
   *             new value back to the PhoneLookupHistory.
   *       </ul>
   * </ul>
   */
  @Override
  public ListenableFuture<Void> fill(CallLogMutations mutations) {
    LogUtil.v(
        "PhoneLookupDataSource.fill",
        "processing mutations (inserts: %d, updates: %d, deletes: %d)",
        mutations.getInserts().size(),
        mutations.getUpdates().size(),
        mutations.getDeletes().size());

    // Clear state saved since the last call to fill. This is necessary in case fill is called but
    // onSuccessfulFill is not called during a previous flow.
    phoneLookupHistoryRowsToUpdate.clear();
    phoneLookupHistoryRowsToDelete.clear();

    // First query information from annotated call log (and include pending inserts).
    ListenableFuture<Map<DialerPhoneNumber, Set<Long>>> annotatedCallLogIdsByNumberFuture =
        backgroundExecutorService.submit(
            () -> collectIdAndNumberFromAnnotatedCallLogAndPendingInserts(appContext, mutations));

    // Use it to create the original info map.
    ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> originalInfoMapFuture =
        Futures.transform(
            annotatedCallLogIdsByNumberFuture,
            annotatedCallLogIdsByNumber ->
                queryPhoneLookupHistoryForNumbers(appContext, annotatedCallLogIdsByNumber.keySet()),
            backgroundExecutorService);

    // Use the original info map to generate the updated info map by delegating to
    // compositePhoneLookup.
    ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> updatedInfoMapFuture =
        Futures.transformAsync(
            originalInfoMapFuture,
            compositePhoneLookup::getMostRecentInfo,
            lightweightExecutorService);

    // This is the computation that will use the result of all of the above.
    Callable<ImmutableMap<Long, PhoneLookupInfo>> computeRowsToUpdate =
        () -> {
          // These get() calls are safe because we are using whenAllSucceed below.
          Map<DialerPhoneNumber, Set<Long>> annotatedCallLogIdsByNumber =
              annotatedCallLogIdsByNumberFuture.get();
          ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> originalInfoMap =
              originalInfoMapFuture.get();
          ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> updatedInfoMap =
              updatedInfoMapFuture.get();

          // First populate the insert mutations
          ImmutableMap.Builder<Long, PhoneLookupInfo>
              originalPhoneLookupHistoryDataByAnnotatedCallLogId = ImmutableMap.builder();
          for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : originalInfoMap.entrySet()) {
            DialerPhoneNumber dialerPhoneNumber = entry.getKey();
            PhoneLookupInfo phoneLookupInfo = entry.getValue();
            for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) {
              originalPhoneLookupHistoryDataByAnnotatedCallLogId.put(id, phoneLookupInfo);
            }
          }
          populateInserts(originalPhoneLookupHistoryDataByAnnotatedCallLogId.build(), mutations);

          // Compute and save the PhoneLookupHistory rows which can be deleted in onSuccessfulFill.
          phoneLookupHistoryRowsToDelete.addAll(
              computePhoneLookupHistoryRowsToDelete(annotatedCallLogIdsByNumber, mutations));

          // Now compute the rows to update.
          ImmutableMap.Builder<Long, PhoneLookupInfo> rowsToUpdate = ImmutableMap.builder();
          for (Entry<DialerPhoneNumber, PhoneLookupInfo> entry : updatedInfoMap.entrySet()) {
            DialerPhoneNumber dialerPhoneNumber = entry.getKey();
            PhoneLookupInfo upToDateInfo = entry.getValue();
            if (!originalInfoMap.get(dialerPhoneNumber).equals(upToDateInfo)) {
              for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) {
                rowsToUpdate.put(id, upToDateInfo);
              }
              // Also save the updated information so that it can be written to PhoneLookupHistory
              // in onSuccessfulFill.
              // Note: This loses country info when number is not valid.
              String normalizedNumber = dialerPhoneNumber.getNormalizedNumber();
              phoneLookupHistoryRowsToUpdate.put(normalizedNumber, upToDateInfo);
            }
          }
          return rowsToUpdate.build();
        };

    ListenableFuture<ImmutableMap<Long, PhoneLookupInfo>> rowsToUpdateFuture =
        Futures.whenAllSucceed(
                annotatedCallLogIdsByNumberFuture, updatedInfoMapFuture, originalInfoMapFuture)
            .call(
                computeRowsToUpdate,
                backgroundExecutorService /* PhoneNumberUtil may do disk IO */);

    // Finally update the mutations with the computed rows.
    return Futures.transform(
        rowsToUpdateFuture,
        rowsToUpdate -> {
          updateMutations(rowsToUpdate, mutations);
          LogUtil.v(
              "PhoneLookupDataSource.fill",
              "updated mutations (inserts: %d, updates: %d, deletes: %d)",
              mutations.getInserts().size(),
              mutations.getUpdates().size(),
              mutations.getDeletes().size());
          return null;
        },
        lightweightExecutorService);
  }

  @Override
  public ListenableFuture<Void> onSuccessfulFill() {
    // First update and/or delete the appropriate rows in PhoneLookupHistory.
    ListenableFuture<Void> writePhoneLookupHistory =
        backgroundExecutorService.submit(() -> writePhoneLookupHistory(appContext));

    // If that succeeds, delegate to the composite PhoneLookup to notify all PhoneLookups that both
    // the AnnotatedCallLog and PhoneLookupHistory have been successfully updated.
    return Futures.transformAsync(
        writePhoneLookupHistory,
        unused -> compositePhoneLookup.onSuccessfulBulkUpdate(),
        lightweightExecutorService);
  }

  @WorkerThread
  private Void writePhoneLookupHistory(Context appContext)
      throws RemoteException, OperationApplicationException {
    ArrayList<ContentProviderOperation> operations = new ArrayList<>();
    long currentTimestamp = System.currentTimeMillis();
    for (Entry<String, PhoneLookupInfo> entry : phoneLookupHistoryRowsToUpdate.entrySet()) {
      String normalizedNumber = entry.getKey();
      PhoneLookupInfo phoneLookupInfo = entry.getValue();
      ContentValues contentValues = new ContentValues();
      contentValues.put(PhoneLookupHistory.PHONE_LOOKUP_INFO, phoneLookupInfo.toByteArray());
      contentValues.put(PhoneLookupHistory.LAST_MODIFIED, currentTimestamp);
      operations.add(
          ContentProviderOperation.newUpdate(
                  PhoneLookupHistory.contentUriForNumber(normalizedNumber))
              .withValues(contentValues)
              .build());
    }
    for (String normalizedNumber : phoneLookupHistoryRowsToDelete) {
      operations.add(
          ContentProviderOperation.newDelete(
                  PhoneLookupHistory.contentUriForNumber(normalizedNumber))
              .build());
    }
    Assert.isNotNull(
        appContext
            .getContentResolver()
            .applyBatch(PhoneLookupHistoryContract.AUTHORITY, operations));
    return null;
  }

  @MainThread
  @Override
  public void registerContentObservers() {
    compositePhoneLookup.registerContentObservers();
  }

  @Override
  public void unregisterContentObservers() {
    compositePhoneLookup.unregisterContentObservers();
  }

  @Override
  public ListenableFuture<Void> clearData() {
    ListenableFuture<Void> clearDataFuture = compositePhoneLookup.clearData();
    ListenableFuture<Void> deleteDatabaseFuture = phoneLookupHistoryDatabaseHelper.delete();

    return Futures.transform(
        Futures.allAsList(clearDataFuture, deleteDatabaseFuture),
        unused -> null,
        MoreExecutors.directExecutor());
  }

  @Override
  public String getLoggingName() {
    return "PhoneLookupDataSource";
  }

  private static ImmutableSet<DialerPhoneNumber>
      queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(Context appContext) {
    ImmutableSet.Builder<DialerPhoneNumber> numbers = ImmutableSet.builder();

    try (Cursor cursor =
        appContext
            .getContentResolver()
            .query(
                AnnotatedCallLog.DISTINCT_NUMBERS_CONTENT_URI,
                new String[] {AnnotatedCallLog.NUMBER},
                null,
                null,
                null)) {

      if (cursor == null) {
        LogUtil.e(
            "PhoneLookupDataSource.queryDistinctDialerPhoneNumbersFromAnnotatedCallLog",
            "null cursor");
        return numbers.build();
      }

      if (cursor.moveToFirst()) {
        int numberColumn = cursor.getColumnIndexOrThrow(AnnotatedCallLog.NUMBER);
        do {
          byte[] blob = cursor.getBlob(numberColumn);
          if (blob == null) {
            // Not all [incoming] calls have associated phone numbers.
            continue;
          }
          try {
            numbers.add(DialerPhoneNumber.parseFrom(blob));
          } catch (InvalidProtocolBufferException e) {
            throw new IllegalStateException(e);
          }
        } while (cursor.moveToNext());
      }
    }
    return numbers.build();
  }

  private Map<DialerPhoneNumber, Set<Long>> collectIdAndNumberFromAnnotatedCallLogAndPendingInserts(
      Context appContext, CallLogMutations mutations) {
    Map<DialerPhoneNumber, Set<Long>> idsByNumber = new ArrayMap<>();
    // First add any pending inserts to the map.
    for (Entry<Long, ContentValues> entry : mutations.getInserts().entrySet()) {
      long id = entry.getKey();
      ContentValues insertedContentValues = entry.getValue();
      DialerPhoneNumber dialerPhoneNumber;
      try {
        dialerPhoneNumber =
            DialerPhoneNumber.parseFrom(
                insertedContentValues.getAsByteArray(AnnotatedCallLog.NUMBER));
      } catch (InvalidProtocolBufferException e) {
        throw new IllegalStateException(e);
      }
      Set<Long> ids = idsByNumber.get(dialerPhoneNumber);
      if (ids == null) {
        ids = new ArraySet<>();
        idsByNumber.put(dialerPhoneNumber, ids);
      }
      ids.add(id);
    }

    try (Cursor cursor =
        appContext
            .getContentResolver()
            .query(
                AnnotatedCallLog.CONTENT_URI,
                new String[] {AnnotatedCallLog._ID, AnnotatedCallLog.NUMBER},
                null,
                null,
                null)) {

      if (cursor == null) {
        LogUtil.e(
            "PhoneLookupDataSource.collectIdAndNumberFromAnnotatedCallLogAndPendingInserts",
            "null cursor");
        return ImmutableMap.of();
      }

      if (cursor.moveToFirst()) {
        int idColumn = cursor.getColumnIndexOrThrow(AnnotatedCallLog._ID);
        int numberColumn = cursor.getColumnIndexOrThrow(AnnotatedCallLog.NUMBER);
        do {
          long id = cursor.getLong(idColumn);
          byte[] blob = cursor.getBlob(numberColumn);
          if (blob == null) {
            // Not all [incoming] calls have associated phone numbers.
            continue;
          }
          DialerPhoneNumber dialerPhoneNumber;
          try {
            dialerPhoneNumber = DialerPhoneNumber.parseFrom(blob);
          } catch (InvalidProtocolBufferException e) {
            throw new IllegalStateException(e);
          }
          Set<Long> ids = idsByNumber.get(dialerPhoneNumber);
          if (ids == null) {
            ids = new ArraySet<>();
            idsByNumber.put(dialerPhoneNumber, ids);
          }
          ids.add(id);
        } while (cursor.moveToNext());
      }
    }
    return idsByNumber;
  }

  /** Returned map must have same keys as {@code uniqueDialerPhoneNumbers} */
  private ImmutableMap<DialerPhoneNumber, PhoneLookupInfo> queryPhoneLookupHistoryForNumbers(
      Context appContext, Set<DialerPhoneNumber> uniqueDialerPhoneNumbers) {
    // Note: This loses country info when number is not valid.
    Map<DialerPhoneNumber, String> dialerPhoneNumberToNormalizedNumbers =
        Maps.asMap(uniqueDialerPhoneNumbers, DialerPhoneNumber::getNormalizedNumber);

    // Convert values to a set to remove any duplicates that are the result of two
    // DialerPhoneNumbers mapping to the same normalized number.
    String[] normalizedNumbers =
        dialerPhoneNumberToNormalizedNumbers.values().toArray(new String[] {});
    String[] questionMarks = new String[normalizedNumbers.length];
    Arrays.fill(questionMarks, "?");
    String selection =
        PhoneLookupHistory.NORMALIZED_NUMBER + " in (" + TextUtils.join(",", questionMarks) + ")";

    Map<String, PhoneLookupInfo> normalizedNumberToInfoMap = new ArrayMap<>();
    try (Cursor cursor =
        appContext
            .getContentResolver()
            .query(
                PhoneLookupHistory.CONTENT_URI,
                new String[] {
                  PhoneLookupHistory.NORMALIZED_NUMBER, PhoneLookupHistory.PHONE_LOOKUP_INFO,
                },
                selection,
                normalizedNumbers,
                null)) {
      if (cursor == null) {
        LogUtil.e("PhoneLookupDataSource.queryPhoneLookupHistoryForNumbers", "null cursor");
      } else if (cursor.moveToFirst()) {
        int normalizedNumberColumn =
            cursor.getColumnIndexOrThrow(PhoneLookupHistory.NORMALIZED_NUMBER);
        int phoneLookupInfoColumn =
            cursor.getColumnIndexOrThrow(PhoneLookupHistory.PHONE_LOOKUP_INFO);
        do {
          String normalizedNumber = cursor.getString(normalizedNumberColumn);
          PhoneLookupInfo phoneLookupInfo;
          try {
            phoneLookupInfo = PhoneLookupInfo.parseFrom(cursor.getBlob(phoneLookupInfoColumn));
          } catch (InvalidProtocolBufferException e) {
            throw new IllegalStateException(e);
          }
          normalizedNumberToInfoMap.put(normalizedNumber, phoneLookupInfo);
        } while (cursor.moveToNext());
      }
    }

    // We have the required information in normalizedNumberToInfoMap but it's keyed by normalized
    // number instead of DialerPhoneNumber. Build and return a new map keyed by DialerPhoneNumber.
    return ImmutableMap.copyOf(
        Maps.asMap(
            uniqueDialerPhoneNumbers,
            (dialerPhoneNumber) -> {
              String normalizedNumber = dialerPhoneNumberToNormalizedNumbers.get(dialerPhoneNumber);
              PhoneLookupInfo phoneLookupInfo = normalizedNumberToInfoMap.get(normalizedNumber);
              // If data is cleared or for other reasons, the PhoneLookupHistory may not contain an
              // entry for a number. Just use an empty value for that case.
              return phoneLookupInfo == null
                  ? PhoneLookupInfo.getDefaultInstance()
                  : phoneLookupInfo;
            }));
  }

  private void populateInserts(
      ImmutableMap<Long, PhoneLookupInfo> existingInfo, CallLogMutations mutations) {
    for (Entry<Long, ContentValues> entry : mutations.getInserts().entrySet()) {
      long id = entry.getKey();
      ContentValues contentValues = entry.getValue();
      PhoneLookupInfo phoneLookupInfo = existingInfo.get(id);
      // Existing info might be missing if data was cleared or for other reasons.
      if (phoneLookupInfo != null) {
        updateContentValues(contentValues, phoneLookupInfo);
      }
    }
  }

  private void updateMutations(
      ImmutableMap<Long, PhoneLookupInfo> updatesToApply, CallLogMutations mutations) {
    for (Entry<Long, PhoneLookupInfo> entry : updatesToApply.entrySet()) {
      long id = entry.getKey();
      PhoneLookupInfo phoneLookupInfo = entry.getValue();
      ContentValues contentValuesToInsert = mutations.getInserts().get(id);
      if (contentValuesToInsert != null) {
        /*
         * This is a confusing case. Consider:
         *
         * 1) An incoming call from "Bob" arrives; "Bob" is written to PhoneLookupHistory.
         * 2) User changes Bob's name to "Robert".
         * 3) User opens call log, and this code is invoked with the inserted call as a mutation.
         *
         * In populateInserts, we retrieved "Bob" from PhoneLookupHistory and wrote it to the insert
         * mutation, which is wrong. We need to actually ask the phone lookups for the most up to
         * date information ("Robert"), and update the "insert" mutation again.
         *
         * Having understood this, you may wonder why populateInserts() is needed at all--excellent
         * question! Consider:
         *
         * 1) An incoming call from number 123 ("Bob") arrives at time T1; "Bob" is written to
         * PhoneLookupHistory.
         * 2) User opens call log at time T2 and "Bob" is written to it, and everything is fine; the
         * call log can be considered accurate as of T2.
         * 3) An incoming call from number 456 ("John") arrives at time T3. Let's say the contact
         * info for John was last modified at time T0.
         * 4) Now imagine that populateInserts() didn't exist; the phone lookup will ask for any
         * information for phone number 456 which has changed since T2--but "John" hasn't changed
         * since then so no contact information would be found.
         *
         * The populateInserts() method avoids this problem by always first populating inserted
         * mutations from PhoneLookupHistory; in this case "John" would be copied during
         * populateInserts() and there wouldn't be further updates needed here.
         */
        updateContentValues(contentValuesToInsert, phoneLookupInfo);
        continue;
      }
      ContentValues contentValuesToUpdate = mutations.getUpdates().get(id);
      if (contentValuesToUpdate != null) {
        updateContentValues(contentValuesToUpdate, phoneLookupInfo);
        continue;
      }
      // Else this row is not already scheduled for insert or update and we need to schedule it.
      ContentValues contentValues = new ContentValues();
      updateContentValues(contentValues, phoneLookupInfo);
      mutations.getUpdates().put(id, contentValues);
    }
  }

  private Set<String> computePhoneLookupHistoryRowsToDelete(
      Map<DialerPhoneNumber, Set<Long>> annotatedCallLogIdsByNumber, CallLogMutations mutations) {
    if (mutations.getDeletes().isEmpty()) {
      return ImmutableSet.of();
    }
    // First convert the dialer phone numbers to normalized numbers; we need to combine entries
    // because different DialerPhoneNumbers can map to the same normalized number.
    Map<String, Set<Long>> idsByNormalizedNumber = new ArrayMap<>();
    for (Entry<DialerPhoneNumber, Set<Long>> entry : annotatedCallLogIdsByNumber.entrySet()) {
      DialerPhoneNumber dialerPhoneNumber = entry.getKey();
      Set<Long> idsForDialerPhoneNumber = entry.getValue();
      // Note: This loses country info when number is not valid.
      String normalizedNumber = dialerPhoneNumber.getNormalizedNumber();
      Set<Long> idsForNormalizedNumber = idsByNormalizedNumber.get(normalizedNumber);
      if (idsForNormalizedNumber == null) {
        idsForNormalizedNumber = new ArraySet<>();
        idsByNormalizedNumber.put(normalizedNumber, idsForNormalizedNumber);
      }
      idsForNormalizedNumber.addAll(idsForDialerPhoneNumber);
    }
    // Now look through and remove all IDs that were scheduled for delete; after doing that, if
    // there are no remaining IDs left for a normalized number, the number can be deleted from
    // PhoneLookupHistory.
    Set<String> normalizedNumbersToDelete = new ArraySet<>();
    for (Entry<String, Set<Long>> entry : idsByNormalizedNumber.entrySet()) {
      String normalizedNumber = entry.getKey();
      Set<Long> idsForNormalizedNumber = entry.getValue();
      idsForNormalizedNumber.removeAll(mutations.getDeletes());
      if (idsForNormalizedNumber.isEmpty()) {
        normalizedNumbersToDelete.add(normalizedNumber);
      }
    }
    return normalizedNumbersToDelete;
  }

  private void updateContentValues(ContentValues contentValues, PhoneLookupInfo phoneLookupInfo) {
    contentValues.put(
        AnnotatedCallLog.NUMBER_ATTRIBUTES,
        NumberAttributesBuilder.fromPhoneLookupInfo(phoneLookupInfo).build().toByteArray());
  }
}
