From 9ee59d50955fc4664a3d312c39340b06a738bfd6 Mon Sep 17 00:00:00 2001 From: zachh Date: Wed, 29 Nov 2017 15:55:47 -0800 Subject: Implemented fill for PhoneLookupDataSource. Rewrote FakePhoneLookup to be more realistic. Bug: 34672501 Test: unit PiperOrigin-RevId: 177376374 Change-Id: Ifcd52b16b7046f39d1bfc0e8b8e76452a9daadd2 --- .../phonelookup/PhoneLookupDataSource.java | 296 ++++++++++++++++++++- 1 file changed, 295 insertions(+), 1 deletion(-) (limited to 'java/com/android/dialer/calllog/datasources') diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java index 90298a104..b3bbf3cbd 100644 --- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java +++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java @@ -22,16 +22,30 @@ import android.content.SharedPreferences; import android.database.Cursor; 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.common.LogUtil; import com.android.dialer.phonelookup.PhoneLookup; +import com.android.dialer.phonelookup.PhoneLookupInfo; +import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; +import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.android.dialer.storage.Unencrypted; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.protobuf.InvalidProtocolBufferException; +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.ExecutionException; import javax.inject.Inject; @@ -71,10 +85,87 @@ public final class PhoneLookupDataSource implements CallLogDataSource { } } + /** + * {@inheritDoc} + * + *

This method uses the following algorithm: + * + *

+ */ @WorkerThread @Override public void fill(Context appContext, CallLogMutations mutations) { - // TODO(zachh): Implementation. + Map> annotatedCallLogIdsByNumber = + queryIdAndNumberFromAnnotatedCallLog(appContext); + Map originalPhoneLookupHistoryDataByNumber = + queryPhoneLookupHistoryForNumbers(appContext, annotatedCallLogIdsByNumber.keySet()); + ImmutableMap.Builder originalPhoneLookupHistoryDataByAnnotatedCallLogId = + ImmutableMap.builder(); + for (Entry entry : + originalPhoneLookupHistoryDataByNumber.entrySet()) { + DialerPhoneNumber dialerPhoneNumber = entry.getKey(); + PhoneLookupInfoAndTimestamp phoneLookupInfoAndTimestamp = entry.getValue(); + for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { + originalPhoneLookupHistoryDataByAnnotatedCallLogId.put( + id, phoneLookupInfoAndTimestamp.phoneLookupInfo()); + } + } + populateInserts(originalPhoneLookupHistoryDataByAnnotatedCallLogId.build(), mutations); + + ImmutableMap originalPhoneLookupInfosByNumber = + ImmutableMap.copyOf( + Maps.transformValues( + originalPhoneLookupHistoryDataByNumber, + PhoneLookupInfoAndTimestamp::phoneLookupInfo)); + + long lastTimestampProcessedSharedPrefValue = + sharedPreferences.getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L); + // TODO(zachh): Push last timestamp processed down into each individual lookup. + ImmutableMap updatedInfoMap; + try { + updatedInfoMap = + phoneLookup + .bulkUpdate(originalPhoneLookupInfosByNumber, lastTimestampProcessedSharedPrefValue) + .get(); + } catch (InterruptedException | ExecutionException e) { + throw new IllegalStateException(e); + } + ImmutableMap.Builder rowsToUpdate = ImmutableMap.builder(); + for (Entry entry : updatedInfoMap.entrySet()) { + DialerPhoneNumber dialerPhoneNumber = entry.getKey(); + PhoneLookupInfo upToDateInfo = entry.getValue(); + long numberLastModified = + originalPhoneLookupHistoryDataByNumber.get(dialerPhoneNumber).lastModified(); + if (numberLastModified > lastTimestampProcessedSharedPrefValue + || !originalPhoneLookupInfosByNumber.get(dialerPhoneNumber).equals(upToDateInfo)) { + for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { + rowsToUpdate.put(id, upToDateInfo); + } + } + } + updateMutations(rowsToUpdate.build(), mutations); } @WorkerThread @@ -136,4 +227,207 @@ public final class PhoneLookupDataSource implements CallLogDataSource { } return numbers.build(); } + + private Map> queryIdAndNumberFromAnnotatedCallLog( + Context appContext) { + Map> idsByNumber = new ArrayMap<>(); + + try (Cursor cursor = + appContext + .getContentResolver() + .query( + AnnotatedCallLog.CONTENT_URI, + new String[] {AnnotatedCallLog._ID, AnnotatedCallLog.NUMBER}, + null, + null, + null)) { + + if (cursor == null) { + LogUtil.e("PhoneLookupDataSource.queryIdAndNumberFromAnnotatedCallLog", "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 ids = idsByNumber.get(dialerPhoneNumber); + if (ids == null) { + ids = new ArraySet<>(); + idsByNumber.put(dialerPhoneNumber, ids); + } + ids.add(id); + } while (cursor.moveToNext()); + } + } + return idsByNumber; + } + + private Map queryPhoneLookupHistoryForNumbers( + Context appContext, Set uniqueDialerPhoneNumbers) { + DialerPhoneNumberUtil dialerPhoneNumberUtil = + new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()); + Map dialerPhoneNumberToNormalizedNumbers = + Maps.asMap(uniqueDialerPhoneNumbers, dialerPhoneNumberUtil::formatToE164); + + // 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 normalizedNumberToInfoMap = new ArrayMap<>(); + try (Cursor cursor = + appContext + .getContentResolver() + .query( + PhoneLookupHistory.CONTENT_URI, + new String[] { + PhoneLookupHistory.NORMALIZED_NUMBER, + PhoneLookupHistory.PHONE_LOOKUP_INFO, + PhoneLookupHistory.LAST_MODIFIED + }, + selection, + normalizedNumbers, + null)) { + + if (cursor == null) { + LogUtil.e("PhoneLookupDataSource.queryPhoneLookupHistoryForNumbers", "null cursor"); + return ImmutableMap.of(); + } + + if (cursor.moveToFirst()) { + int normalizedNumberColumn = + cursor.getColumnIndexOrThrow(PhoneLookupHistory.NORMALIZED_NUMBER); + int phoneLookupInfoColumn = + cursor.getColumnIndexOrThrow(PhoneLookupHistory.PHONE_LOOKUP_INFO); + int lastModifiedColumn = cursor.getColumnIndexOrThrow(PhoneLookupHistory.LAST_MODIFIED); + do { + String normalizedNumber = cursor.getString(normalizedNumberColumn); + PhoneLookupInfo phoneLookupInfo; + try { + phoneLookupInfo = PhoneLookupInfo.parseFrom(cursor.getBlob(phoneLookupInfoColumn)); + } catch (InvalidProtocolBufferException e) { + throw new IllegalStateException(e); + } + long lastModified = cursor.getLong(lastModifiedColumn); + normalizedNumberToInfoMap.put( + normalizedNumber, PhoneLookupInfoAndTimestamp.create(phoneLookupInfo, lastModified)); + } 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 Maps.asMap( + uniqueDialerPhoneNumbers, + (dialerPhoneNumber) -> { + String normalizedNumber = dialerPhoneNumberToNormalizedNumbers.get(dialerPhoneNumber); + PhoneLookupInfoAndTimestamp infoAndTimestamp = + 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 infoAndTimestamp == null + ? PhoneLookupInfoAndTimestamp.create(PhoneLookupInfo.getDefaultInstance(), 0L) + : infoAndTimestamp; + }); + } + + private static void populateInserts( + ImmutableMap existingInfo, CallLogMutations mutations) { + for (Entry 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) { + contentValues.put(AnnotatedCallLog.NAME, selectName(phoneLookupInfo)); + } + } + } + + private static void updateMutations( + ImmutableMap updatesToApply, CallLogMutations mutations) { + for (Entry 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. + */ + contentValuesToInsert.put(AnnotatedCallLog.NAME, selectName(phoneLookupInfo)); + continue; + } + ContentValues contentValuesToUpdate = mutations.getUpdates().get(id); + if (contentValuesToUpdate != null) { + contentValuesToUpdate.put(AnnotatedCallLog.NAME, selectName(phoneLookupInfo)); + continue; + } + // Else this row is not already scheduled for insert or update and we need to schedule it. + ContentValues contentValues = new ContentValues(); + contentValues.put(AnnotatedCallLog.NAME, selectName(phoneLookupInfo)); + mutations.getUpdates().put(id, contentValues); + } + } + + // TODO(zachh): Extract this logic into a proper selection class or package. + private static String selectName(PhoneLookupInfo phoneLookupInfo) { + if (phoneLookupInfo.getCp2Info().getCp2ContactInfoCount() > 0) { + return phoneLookupInfo.getCp2Info().getCp2ContactInfo(0).getName(); + } + return ""; + } + + @AutoValue + abstract static class PhoneLookupInfoAndTimestamp { + abstract PhoneLookupInfo phoneLookupInfo(); + + abstract long lastModified(); + + static PhoneLookupInfoAndTimestamp create(PhoneLookupInfo phoneLookupInfo, long lastModified) { + return new AutoValue_PhoneLookupDataSource_PhoneLookupInfoAndTimestamp( + phoneLookupInfo, lastModified); + } + } } -- cgit v1.2.3