diff options
Diffstat (limited to 'java/com/android/dialer/phonelookup/cp2')
3 files changed, 385 insertions, 86 deletions
diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java index 727977f5c..8879fee06 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java @@ -66,45 +66,7 @@ import javax.inject.Inject; public final class Cp2LocalPhoneLookup implements PhoneLookup<Cp2Info> { private static final String PREF_LAST_TIMESTAMP_PROCESSED = - "cp2PhoneLookupLastTimestampProcessed"; - - /** Projection for performing batch lookups based on E164 numbers using the PHONE table. */ - private static final String[] PHONE_PROJECTION = - new String[] { - Phone.DISPLAY_NAME_PRIMARY, // 0 - Phone.PHOTO_THUMBNAIL_URI, // 1 - Phone.PHOTO_ID, // 2 - Phone.TYPE, // 3 - Phone.LABEL, // 4 - Phone.NORMALIZED_NUMBER, // 5 - Phone.CONTACT_ID, // 6 - Phone.LOOKUP_KEY // 7 - }; - - /** - * Projection for performing individual lookups of non-E164 numbers using the PHONE_LOOKUP table. - */ - private static final String[] PHONE_LOOKUP_PROJECTION = - new String[] { - ContactsContract.PhoneLookup.DISPLAY_NAME_PRIMARY, // 0 - ContactsContract.PhoneLookup.PHOTO_THUMBNAIL_URI, // 1 - ContactsContract.PhoneLookup.PHOTO_ID, // 2 - ContactsContract.PhoneLookup.TYPE, // 3 - ContactsContract.PhoneLookup.LABEL, // 4 - ContactsContract.PhoneLookup.NORMALIZED_NUMBER, // 5 - ContactsContract.PhoneLookup.CONTACT_ID, // 6 - ContactsContract.PhoneLookup.LOOKUP_KEY // 7 - }; - - // The following indexes should match both PHONE_PROJECTION and PHONE_LOOKUP_PROJECTION above. - private static final int CP2_INFO_NAME_INDEX = 0; - private static final int CP2_INFO_PHOTO_URI_INDEX = 1; - private static final int CP2_INFO_PHOTO_ID_INDEX = 2; - private static final int CP2_INFO_TYPE_INDEX = 3; - private static final int CP2_INFO_LABEL_INDEX = 4; - private static final int CP2_INFO_NORMALIZED_NUMBER_INDEX = 5; - private static final int CP2_INFO_CONTACT_ID_INDEX = 6; - private static final int CP2_INFO_LOOKUP_KEY_INDEX = 7; + "cp2LocalPhoneLookupLastTimestampProcessed"; // We cannot efficiently process invalid numbers because batch queries cannot be constructed which // accomplish the necessary loose matching. We'll attempt to process a limited number of them, @@ -146,14 +108,15 @@ public final class Cp2LocalPhoneLookup implements PhoneLookup<Cp2Info> { // ensure consistency when the batch methods are used to update data. try (Cursor cursor = e164.isPresent() - ? queryPhoneTableBasedOnE164(PHONE_PROJECTION, ImmutableSet.of(e164.get())) - : queryPhoneLookup(PHONE_LOOKUP_PROJECTION, rawNumber)) { + ? queryPhoneTableBasedOnE164( + Cp2Projections.getProjectionForPhoneTable(), ImmutableSet.of(e164.get())) + : queryPhoneLookup(Cp2Projections.getProjectionForPhoneLookupTable(), rawNumber)) { if (cursor == null) { LogUtil.w("Cp2LocalPhoneLookup.lookupInternal", "null cursor"); return Cp2Info.getDefaultInstance(); } while (cursor.moveToNext()) { - cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); + cp2ContactInfos.add(Cp2Projections.buildCp2ContactInfoFromCursor(appContext, cursor)); } } return Cp2Info.newBuilder().addAllCp2ContactInfo(cp2ContactInfos).build(); @@ -174,13 +137,14 @@ public final class Cp2LocalPhoneLookup implements PhoneLookup<Cp2Info> { return Cp2Info.getDefaultInstance(); } Set<Cp2ContactInfo> cp2ContactInfos = new ArraySet<>(); - try (Cursor cursor = queryPhoneLookup(PHONE_LOOKUP_PROJECTION, rawNumber)) { + try (Cursor cursor = + queryPhoneLookup(Cp2Projections.getProjectionForPhoneLookupTable(), rawNumber)) { if (cursor == null) { LogUtil.w("Cp2LocalPhoneLookup.lookup", "null cursor"); return Cp2Info.getDefaultInstance(); } while (cursor.moveToNext()) { - cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); + cp2ContactInfos.add(Cp2Projections.buildCp2ContactInfoFromCursor(appContext, cursor)); } } return Cp2Info.newBuilder().addAllCp2ContactInfo(cp2ContactInfos).build(); @@ -767,18 +731,21 @@ public final class Cp2LocalPhoneLookup implements PhoneLookup<Cp2Info> { if (e164Numbers.isEmpty()) { return cp2ContactInfosByNumber; } - try (Cursor cursor = queryPhoneTableBasedOnE164(PHONE_PROJECTION, e164Numbers)) { + try (Cursor cursor = + queryPhoneTableBasedOnE164( + Cp2Projections.getProjectionForPhoneTable(), e164Numbers)) { if (cursor == null) { LogUtil.w("Cp2LocalPhoneLookup.batchQueryForValidNumbers", "null cursor"); } else { while (cursor.moveToNext()) { - String e164Number = cursor.getString(CP2_INFO_NORMALIZED_NUMBER_INDEX); + String e164Number = Cp2Projections.getNormalizedNumberFromCursor(cursor); Set<Cp2ContactInfo> cp2ContactInfos = cp2ContactInfosByNumber.get(e164Number); if (cp2ContactInfos == null) { cp2ContactInfos = new ArraySet<>(); cp2ContactInfosByNumber.put(e164Number, cp2ContactInfos); } - cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); + cp2ContactInfos.add( + Cp2Projections.buildCp2ContactInfoFromCursor(appContext, cursor)); } } } @@ -794,12 +761,14 @@ public final class Cp2LocalPhoneLookup implements PhoneLookup<Cp2Info> { if (invalidNumber.isEmpty()) { return cp2ContactInfos; } - try (Cursor cursor = queryPhoneLookup(PHONE_LOOKUP_PROJECTION, invalidNumber)) { + try (Cursor cursor = + queryPhoneLookup(Cp2Projections.getProjectionForPhoneLookupTable(), invalidNumber)) { if (cursor == null) { LogUtil.w("Cp2LocalPhoneLookup.individualQueryForInvalidNumber", "null cursor"); } else { while (cursor.moveToNext()) { - cp2ContactInfos.add(buildCp2ContactInfoFromPhoneCursor(appContext, cursor)); + cp2ContactInfos.add( + Cp2Projections.buildCp2ContactInfoFromCursor(appContext, cursor)); } } } @@ -843,43 +812,6 @@ public final class Cp2LocalPhoneLookup implements PhoneLookup<Cp2Info> { return appContext.getContentResolver().query(uri, projection, null, null, null); } - /** - * @param cursor with projection {@link #PHONE_PROJECTION}. - * @return new {@link Cp2ContactInfo} based on current row of {@code cursor}. - */ - private static Cp2ContactInfo buildCp2ContactInfoFromPhoneCursor( - Context appContext, Cursor cursor) { - String displayName = cursor.getString(CP2_INFO_NAME_INDEX); - String photoUri = cursor.getString(CP2_INFO_PHOTO_URI_INDEX); - int photoId = cursor.getInt(CP2_INFO_PHOTO_ID_INDEX); - int type = cursor.getInt(CP2_INFO_TYPE_INDEX); - String label = cursor.getString(CP2_INFO_LABEL_INDEX); - int contactId = cursor.getInt(CP2_INFO_CONTACT_ID_INDEX); - String lookupKey = cursor.getString(CP2_INFO_LOOKUP_KEY_INDEX); - - Cp2ContactInfo.Builder infoBuilder = Cp2ContactInfo.newBuilder(); - if (!TextUtils.isEmpty(displayName)) { - infoBuilder.setName(displayName); - } - if (!TextUtils.isEmpty(photoUri)) { - infoBuilder.setPhotoUri(photoUri); - } - if (photoId > 0) { - infoBuilder.setPhotoId(photoId); - } - - // Phone.getTypeLabel returns "Custom" if given (0, null) which is not of any use. Just - // omit setting the label if there's no information for it. - if (type != 0 || !TextUtils.isEmpty(label)) { - infoBuilder.setLabel(Phone.getTypeLabel(appContext.getResources(), type, label).toString()); - } - infoBuilder.setContactId(contactId); - if (!TextUtils.isEmpty(lookupKey)) { - infoBuilder.setLookupUri(Contacts.getLookupUri(contactId, lookupKey).toString()); - } - return infoBuilder.build(); - } - /** Returns set of DialerPhoneNumbers that were associated with now deleted contacts. */ private ListenableFuture<Set<DialerPhoneNumber>> getDeletedPhoneNumbers( ImmutableMap<DialerPhoneNumber, Cp2Info> existingInfoMap, long lastModified) { diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2Projections.java b/java/com/android/dialer/phonelookup/cp2/Cp2Projections.java new file mode 100644 index 000000000..e3929990e --- /dev/null +++ b/java/com/android/dialer/phonelookup/cp2/Cp2Projections.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2018 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.phonelookup.cp2; + +import android.content.Context; +import android.database.Cursor; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.PhoneLookup; +import android.text.TextUtils; +import com.android.dialer.phonelookup.PhoneLookupInfo.Cp2Info.Cp2ContactInfo; + +/** + * A class providing projection-related functionality for {@link + * com.android.dialer.phonelookup.PhoneLookup} implementations for ContactsProvider2 (CP2). + */ +final class Cp2Projections { + + // Projection for performing lookups using the PHONE table + private static final String[] PHONE_PROJECTION = + new String[] { + Phone.DISPLAY_NAME_PRIMARY, // 0 + Phone.PHOTO_THUMBNAIL_URI, // 1 + Phone.PHOTO_ID, // 2 + Phone.TYPE, // 3 + Phone.LABEL, // 4 + Phone.NORMALIZED_NUMBER, // 5 + Phone.CONTACT_ID, // 6 + Phone.LOOKUP_KEY // 7 + }; + + // Projection for performing lookups using the PHONE_LOOKUP table + private static final String[] PHONE_LOOKUP_PROJECTION = + new String[] { + PhoneLookup.DISPLAY_NAME_PRIMARY, // 0 + PhoneLookup.PHOTO_THUMBNAIL_URI, // 1 + PhoneLookup.PHOTO_ID, // 2 + PhoneLookup.TYPE, // 3 + PhoneLookup.LABEL, // 4 + PhoneLookup.NORMALIZED_NUMBER, // 5 + PhoneLookup.CONTACT_ID, // 6 + PhoneLookup.LOOKUP_KEY // 7 + }; + + // The following indexes should match both PHONE_PROJECTION and PHONE_LOOKUP_PROJECTION above. + private static final int CP2_INFO_NAME_INDEX = 0; + private static final int CP2_INFO_PHOTO_URI_INDEX = 1; + private static final int CP2_INFO_PHOTO_ID_INDEX = 2; + private static final int CP2_INFO_TYPE_INDEX = 3; + private static final int CP2_INFO_LABEL_INDEX = 4; + private static final int CP2_INFO_NORMALIZED_NUMBER_INDEX = 5; + private static final int CP2_INFO_CONTACT_ID_INDEX = 6; + private static final int CP2_INFO_LOOKUP_KEY_INDEX = 7; + + private Cp2Projections() {} + + static String[] getProjectionForPhoneTable() { + return PHONE_PROJECTION; + } + + static String[] getProjectionForPhoneLookupTable() { + return PHONE_LOOKUP_PROJECTION; + } + + /** + * Builds a {@link Cp2ContactInfo} based on the current row of {@code cursor}, of which the + * projection is either {@link #PHONE_PROJECTION} or {@link #PHONE_LOOKUP_PROJECTION}. + */ + static Cp2ContactInfo buildCp2ContactInfoFromCursor(Context appContext, Cursor cursor) { + String displayName = cursor.getString(CP2_INFO_NAME_INDEX); + String photoUri = cursor.getString(CP2_INFO_PHOTO_URI_INDEX); + int photoId = cursor.getInt(CP2_INFO_PHOTO_ID_INDEX); + int type = cursor.getInt(CP2_INFO_TYPE_INDEX); + String label = cursor.getString(CP2_INFO_LABEL_INDEX); + int contactId = cursor.getInt(CP2_INFO_CONTACT_ID_INDEX); + String lookupKey = cursor.getString(CP2_INFO_LOOKUP_KEY_INDEX); + + Cp2ContactInfo.Builder infoBuilder = Cp2ContactInfo.newBuilder(); + if (!TextUtils.isEmpty(displayName)) { + infoBuilder.setName(displayName); + } + if (!TextUtils.isEmpty(photoUri)) { + infoBuilder.setPhotoUri(photoUri); + } + if (photoId > 0) { + infoBuilder.setPhotoId(photoId); + } + + // Phone.getTypeLabel returns "Custom" if given (0, null) which is not of any use. Just + // omit setting the label if there's no information for it. + if (type != 0 || !TextUtils.isEmpty(label)) { + infoBuilder.setLabel(Phone.getTypeLabel(appContext.getResources(), type, label).toString()); + } + infoBuilder.setContactId(contactId); + if (!TextUtils.isEmpty(lookupKey)) { + infoBuilder.setLookupUri(Contacts.getLookupUri(contactId, lookupKey).toString()); + } + return infoBuilder.build(); + } + + /** Returns the normalized number in the current row of {@code cursor}. */ + static String getNormalizedNumberFromCursor(Cursor cursor) { + return cursor.getString(CP2_INFO_NORMALIZED_NUMBER_INDEX); + } +} diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2RemotePhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2RemotePhoneLookup.java new file mode 100644 index 000000000..6a4682958 --- /dev/null +++ b/java/com/android/dialer/phonelookup/cp2/Cp2RemotePhoneLookup.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2018 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.phonelookup.cp2; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Directory; +import android.support.annotation.VisibleForTesting; +import android.telecom.Call; +import com.android.dialer.DialerPhoneNumber; +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.PhoneLookupInfo.Cp2Info; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.telecom.TelecomCallUtil; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import java.util.ArrayList; +import java.util.List; +import javax.inject.Inject; + +/** PhoneLookup implementation for remote contacts. */ +public final class Cp2RemotePhoneLookup implements PhoneLookup<Cp2Info> { + + private final Context appContext; + private final ListeningExecutorService backgroundExecutorService; + private final ListeningExecutorService lightweightExecutorService; + + @Inject + Cp2RemotePhoneLookup( + @ApplicationContext Context appContext, + @BackgroundExecutor ListeningExecutorService backgroundExecutorService, + @LightweightExecutor ListeningExecutorService lightweightExecutorService) { + this.appContext = appContext; + this.backgroundExecutorService = backgroundExecutorService; + this.lightweightExecutorService = lightweightExecutorService; + } + + @Override + public ListenableFuture<Cp2Info> lookup(Call call) { + String number = TelecomCallUtil.getNumber(call); + if (number == null) { + return Futures.immediateFuture(Cp2Info.getDefaultInstance()); + } + + return Futures.transformAsync( + queryCp2ForRemoteDirectoryIds(), + remoteDirectoryIds -> queryCp2ForRemoteContact(number, remoteDirectoryIds), + lightweightExecutorService); + } + + private ListenableFuture<List<Long>> queryCp2ForRemoteDirectoryIds() { + return backgroundExecutorService.submit( + () -> { + List<Long> remoteDirectoryIds = new ArrayList<>(); + try (Cursor cursor = + appContext + .getContentResolver() + .query( + getContentUriForDirectoryIds(), + /* projection = */ new String[] {ContactsContract.Directory._ID}, + /* selection = */ null, + /* selectionArgs = */ null, + /* sortOrder = */ ContactsContract.Directory._ID)) { + if (cursor == null) { + LogUtil.e("Cp2RemotePhoneLookup.queryCp2ForDirectoryIds", "null cursor"); + return remoteDirectoryIds; + } + + if (!cursor.moveToFirst()) { + LogUtil.i("Cp2RemotePhoneLookup.queryCp2ForDirectoryIds", "empty cursor"); + return remoteDirectoryIds; + } + + int idColumnIndex = cursor.getColumnIndexOrThrow(ContactsContract.Directory._ID); + do { + long directoryId = cursor.getLong(idColumnIndex); + + // Note that IDs of non-remote directories will be included in the result, such as + // android.provider.ContactsContract.Directory.DEFAULT (the default directory that + // represents locally stored contacts). + if (isRemoteDirectory(directoryId)) { + remoteDirectoryIds.add(cursor.getLong(idColumnIndex)); + } + } while (cursor.moveToNext()); + return remoteDirectoryIds; + } + }); + } + + private ListenableFuture<Cp2Info> queryCp2ForRemoteContact( + String number, List<Long> remoteDirectoryIds) { + if (remoteDirectoryIds.isEmpty()) { + return Futures.immediateFuture(Cp2Info.getDefaultInstance()); + } + + List<ListenableFuture<Cp2Info>> cp2InfoFutures = new ArrayList<>(); + for (long remoteDirectoryId : remoteDirectoryIds) { + cp2InfoFutures.add(queryCp2ForRemoteContact(number, remoteDirectoryId)); + } + + return Futures.transform( + Futures.allAsList(cp2InfoFutures), + cp2InfoList -> { + Cp2Info.Builder cp2InfoBuilder = Cp2Info.newBuilder(); + for (Cp2Info cp2Info : cp2InfoList) { + cp2InfoBuilder.addAllCp2ContactInfo(cp2Info.getCp2ContactInfoList()); + } + return cp2InfoBuilder.build(); + }, + lightweightExecutorService); + } + + private ListenableFuture<Cp2Info> queryCp2ForRemoteContact( + String number, long remoteDirectoryId) { + return backgroundExecutorService.submit( + () -> { + Cp2Info.Builder cp2InfoBuilder = Cp2Info.newBuilder(); + try (Cursor cursor = + appContext + .getContentResolver() + .query( + getContentUriForContacts(number, remoteDirectoryId), + Cp2Projections.getProjectionForPhoneLookupTable(), + /* selection = */ null, + /* selectionArgs = */ null, + /* sortOrder = */ null)) { + if (cursor == null) { + LogUtil.e( + "Cp2RemotePhoneLookup.queryCp2ForRemoteContact", + "null cursor returned when querying directory %d", + remoteDirectoryId); + return cp2InfoBuilder.build(); + } + + if (!cursor.moveToFirst()) { + LogUtil.i( + "Cp2RemotePhoneLookup.queryCp2ForRemoteContact", + "empty cursor returned when querying directory %d", + remoteDirectoryId); + return cp2InfoBuilder.build(); + } + + do { + cp2InfoBuilder.addCp2ContactInfo( + Cp2Projections.buildCp2ContactInfoFromCursor(appContext, cursor)); + } while (cursor.moveToNext()); + } + + return cp2InfoBuilder.build(); + }); + } + + @VisibleForTesting + static Uri getContentUriForDirectoryIds() { + return VERSION.SDK_INT >= VERSION_CODES.N + ? ContactsContract.Directory.ENTERPRISE_CONTENT_URI + : ContactsContract.Directory.CONTENT_URI; + } + + @VisibleForTesting + static Uri getContentUriForContacts(String number, long directoryId) { + Uri baseUri = + VERSION.SDK_INT >= VERSION_CODES.N + ? ContactsContract.PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI + : ContactsContract.PhoneLookup.CONTENT_FILTER_URI; + + Uri.Builder builder = + baseUri + .buildUpon() + .appendPath(number) + .appendQueryParameter( + ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, + String.valueOf(PhoneNumberHelper.isUriNumber(number))) + .appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)); + + return builder.build(); + } + + private static boolean isRemoteDirectory(long directoryId) { + return VERSION.SDK_INT >= VERSION_CODES.N + ? Directory.isRemoteDirectoryId(directoryId) + : (directoryId != Directory.DEFAULT + && directoryId != Directory.LOCAL_INVISIBLE + // Directory.ENTERPRISE_DEFAULT is the default work profile directory for locally stored + // contacts + && directoryId != Directory.ENTERPRISE_DEFAULT + && directoryId != Directory.ENTERPRISE_LOCAL_INVISIBLE); + } + + @Override + public ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers) { + return Futures.immediateFuture(false); + } + + @Override + public ListenableFuture<ImmutableMap<DialerPhoneNumber, Cp2Info>> getMostRecentInfo( + ImmutableMap<DialerPhoneNumber, Cp2Info> existingInfoMap) { + return Futures.immediateFuture(existingInfoMap); + } + + @Override + public void setSubMessage(PhoneLookupInfo.Builder destination, Cp2Info subMessage) { + destination.setCp2RemoteInfo(subMessage); + } + + @Override + public Cp2Info getSubMessage(PhoneLookupInfo phoneLookupInfo) { + return phoneLookupInfo.getCp2RemoteInfo(); + } + + @Override + public ListenableFuture<Void> onSuccessfulBulkUpdate() { + return Futures.immediateFuture(null); + } + + @Override + public void registerContentObservers( + Context appContext, ContentObserverCallbacks contentObserverCallbacks) { + // No content observer needed for remote contacts + } +} |