blob: 1d4a35a6c397be03a43ebd042cab16f153479810 [file] [log] [blame]
/*
* 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.NumberAttributes;
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.calllog.datasources.util.RowCombiner;
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.consolidator.PhoneLookupInfoConsolidator;
import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract;
import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory;
import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
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.i18n.phonenumbers.PhoneNumberUtil;
import com.google.protobuf.InvalidProtocolBufferException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
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, PhoneLookup.ContentObserverCallbacks {
private final Context appContext;
private final PhoneLookup<PhoneLookupInfo> phoneLookup;
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 #fill(Context, CallLogMutations)} and {@link
* #onSuccessfulFill(Context)} 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 #fill(Context, CallLogMutations)} and {@link
* #onSuccessfulFill(Context)} operations.
*/
private final Set<String> phoneLookupHistoryRowsToDelete = new ArraySet<>();
private CallLogDataSource.ContentObserverCallbacks dataSourceContentObserverCallbacks;
@Inject
PhoneLookupDataSource(
PhoneLookup<PhoneLookupInfo> phoneLookup,
@ApplicationContext Context appContext,
@BackgroundExecutor ListeningExecutorService backgroundExecutorService,
@LightweightExecutor ListeningExecutorService lightweightExecutorService) {
this.phoneLookup = phoneLookup;
this.appContext = appContext;
this.backgroundExecutorService = backgroundExecutorService;
this.lightweightExecutorService = lightweightExecutorService;
}
@Override
public ListenableFuture<Boolean> isDirty(Context appContext) {
ListenableFuture<ImmutableSet<DialerPhoneNumber>> phoneNumbers =
backgroundExecutorService.submit(
() -> queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(appContext));
return Futures.transformAsync(phoneNumbers, phoneLookup::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(Context appContext, 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 phoneLookup.
ListenableFuture<ImmutableMap<DialerPhoneNumber, PhoneLookupInfo>> updatedInfoMapFuture =
Futures.transformAsync(
originalInfoMapFuture, phoneLookup::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.
DialerPhoneNumberUtil dialerPhoneNumberUtil =
new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance());
phoneLookupHistoryRowsToDelete.addAll(
computePhoneLookupHistoryRowsToDelete(
annotatedCallLogIdsByNumber, mutations, dialerPhoneNumberUtil));
// 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.
String normalizedNumber = dialerPhoneNumberUtil.normalizeNumber(dialerPhoneNumber);
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(Context appContext) {
// 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 -> phoneLookup.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());
}
appContext.getContentResolver().applyBatch(PhoneLookupHistoryContract.AUTHORITY, operations);
return null;
}
@WorkerThread
@Override
public ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc) {
return new RowCombiner(individualRowsSortedByTimestampDesc)
.useMostRecentBlob(AnnotatedCallLog.NUMBER_ATTRIBUTES)
.combine();
}
@MainThread
@Override
public void registerContentObservers(
Context appContext, CallLogDataSource.ContentObserverCallbacks contentObserverCallbacks) {
dataSourceContentObserverCallbacks = contentObserverCallbacks;
phoneLookup.registerContentObservers(appContext, this);
}
@MainThread
@Override
public void markDirtyAndNotify(Context appContext) {
Assert.isMainThread();
dataSourceContentObserverCallbacks.markDirtyAndNotify(appContext);
}
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) {
DialerPhoneNumberUtil dialerPhoneNumberUtil =
new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance());
Map<DialerPhoneNumber, String> dialerPhoneNumberToNormalizedNumbers =
Maps.asMap(uniqueDialerPhoneNumbers, dialerPhoneNumberUtil::normalizeNumber);
// 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,
DialerPhoneNumberUtil dialerPhoneNumberUtil) {
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();
String normalizedNumber = dialerPhoneNumberUtil.normalizeNumber(dialerPhoneNumber);
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) {
PhoneLookupInfoConsolidator phoneLookupInfoConsolidator =
new PhoneLookupInfoConsolidator(appContext, phoneLookupInfo);
contentValues.put(
AnnotatedCallLog.NUMBER_ATTRIBUTES,
NumberAttributes.newBuilder()
.setName(phoneLookupInfoConsolidator.getName())
.setPhotoUri(phoneLookupInfoConsolidator.getPhotoUri())
.setPhotoId(phoneLookupInfoConsolidator.getPhotoId())
.setLookupUri(phoneLookupInfoConsolidator.getLookupUri())
.setNumberTypeLabel(phoneLookupInfoConsolidator.getNumberLabel())
.setIsBusiness(phoneLookupInfoConsolidator.isBusiness())
.setIsVoicemail(phoneLookupInfoConsolidator.isVoicemail())
.setCanReportAsInvalidNumber(phoneLookupInfoConsolidator.canReportAsInvalidNumber())
.setIsCp2InfoIncomplete(phoneLookupInfoConsolidator.isCp2LocalInfoIncomplete())
.build()
.toByteArray());
}
}