summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/phonenumbercache
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/dialer/phonenumbercache')
-rw-r--r--java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java77
-rw-r--r--java/com/android/dialer/phonenumbercache/CallLogQuery.java107
-rw-r--r--java/com/android/dialer/phonenumbercache/ContactInfo.java165
-rw-r--r--java/com/android/dialer/phonenumbercache/ContactInfoHelper.java586
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java40
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneNumberCache.java50
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java26
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java26
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java29
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneQuery.java96
10 files changed, 1202 insertions, 0 deletions
diff --git a/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
new file mode 100644
index 000000000..03b77b91c
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 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.phonenumbercache;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import java.io.InputStream;
+
+public interface CachedNumberLookupService {
+
+ CachedContactInfo buildCachedContactInfo(ContactInfo info);
+
+ /**
+ * Perform a lookup using the cached number lookup service to return contact information stored in
+ * the cache that corresponds to the given number.
+ *
+ * @param context Valid context
+ * @param number Phone number to lookup the cache for
+ * @return A {@link CachedContactInfo} containing the contact information if the phone number is
+ * found in the cache, {@link ContactInfo#EMPTY} if the phone number was not found in the
+ * cache, and null if there was an error when querying the cache.
+ */
+ CachedContactInfo lookupCachedContactFromNumber(Context context, String number);
+
+ void addContact(Context context, CachedContactInfo info);
+
+ boolean isCacheUri(String uri);
+
+ boolean isBusiness(int sourceType);
+
+ boolean canReportAsInvalid(int sourceType, String objectId);
+
+ /** @return return {@link Uri} to the photo or return {@code null} when failing to add photo */
+ @Nullable
+ Uri addPhoto(Context context, String number, InputStream in);
+
+ /**
+ * Remove all cached phone number entries from the cache, regardless of how old they are.
+ *
+ * @param context Valid context
+ */
+ void clearAllCacheEntries(Context context);
+
+ interface CachedContactInfo {
+
+ int SOURCE_TYPE_DIRECTORY = 1;
+ int SOURCE_TYPE_EXTENDED = 2;
+ int SOURCE_TYPE_PLACES = 3;
+ int SOURCE_TYPE_PROFILE = 4;
+ int SOURCE_TYPE_CNAP = 5;
+
+ ContactInfo getContactInfo();
+
+ void setSource(int sourceType, String name, long directoryId);
+
+ void setDirectorySource(String name, long directoryId);
+
+ void setExtendedSource(String name, long directoryId);
+
+ void setLookupKey(String lookupKey);
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/CallLogQuery.java b/java/com/android/dialer/phonenumbercache/CallLogQuery.java
new file mode 100644
index 000000000..6d4756927
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/CallLogQuery.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2011 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.phonenumbercache;
+
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** The query for the call log table. */
+public final class CallLogQuery {
+
+ public static final int ID = 0;
+ public static final int NUMBER = 1;
+ public static final int DATE = 2;
+ public static final int DURATION = 3;
+ public static final int CALL_TYPE = 4;
+ public static final int COUNTRY_ISO = 5;
+ public static final int VOICEMAIL_URI = 6;
+ public static final int GEOCODED_LOCATION = 7;
+ public static final int CACHED_NAME = 8;
+ public static final int CACHED_NUMBER_TYPE = 9;
+ public static final int CACHED_NUMBER_LABEL = 10;
+ public static final int CACHED_LOOKUP_URI = 11;
+ public static final int CACHED_MATCHED_NUMBER = 12;
+ public static final int CACHED_NORMALIZED_NUMBER = 13;
+ public static final int CACHED_PHOTO_ID = 14;
+ public static final int CACHED_FORMATTED_NUMBER = 15;
+ public static final int IS_READ = 16;
+ public static final int NUMBER_PRESENTATION = 17;
+ public static final int ACCOUNT_COMPONENT_NAME = 18;
+ public static final int ACCOUNT_ID = 19;
+ public static final int FEATURES = 20;
+ public static final int DATA_USAGE = 21;
+ public static final int TRANSCRIPTION = 22;
+ public static final int CACHED_PHOTO_URI = 23;
+
+ @RequiresApi(VERSION_CODES.N)
+ public static final int POST_DIAL_DIGITS = 24;
+
+ @RequiresApi(VERSION_CODES.N)
+ public static final int VIA_NUMBER = 25;
+
+ private static final String[] PROJECTION_M =
+ new String[] {
+ Calls._ID, // 0
+ Calls.NUMBER, // 1
+ Calls.DATE, // 2
+ Calls.DURATION, // 3
+ Calls.TYPE, // 4
+ Calls.COUNTRY_ISO, // 5
+ Calls.VOICEMAIL_URI, // 6
+ Calls.GEOCODED_LOCATION, // 7
+ Calls.CACHED_NAME, // 8
+ Calls.CACHED_NUMBER_TYPE, // 9
+ Calls.CACHED_NUMBER_LABEL, // 10
+ Calls.CACHED_LOOKUP_URI, // 11
+ Calls.CACHED_MATCHED_NUMBER, // 12
+ Calls.CACHED_NORMALIZED_NUMBER, // 13
+ Calls.CACHED_PHOTO_ID, // 14
+ Calls.CACHED_FORMATTED_NUMBER, // 15
+ Calls.IS_READ, // 16
+ Calls.NUMBER_PRESENTATION, // 17
+ Calls.PHONE_ACCOUNT_COMPONENT_NAME, // 18
+ Calls.PHONE_ACCOUNT_ID, // 19
+ Calls.FEATURES, // 20
+ Calls.DATA_USAGE, // 21
+ Calls.TRANSCRIPTION, // 22
+ Calls.CACHED_PHOTO_URI, // 23
+ };
+
+ private static final String[] PROJECTION_N;
+
+ static {
+ List<String> projectionList = new ArrayList<>(Arrays.asList(PROJECTION_M));
+ projectionList.add(CallLog.Calls.POST_DIAL_DIGITS);
+ projectionList.add(CallLog.Calls.VIA_NUMBER);
+ PROJECTION_N = projectionList.toArray(new String[projectionList.size()]);
+ }
+
+ @NonNull
+ public static String[] getProjection() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return PROJECTION_N;
+ }
+ return PROJECTION_M;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/ContactInfo.java b/java/com/android/dialer/phonenumbercache/ContactInfo.java
new file mode 100644
index 000000000..d7a75c34f
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/ContactInfo.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2011 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.phonenumbercache;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.util.UriUtils;
+
+/** Information for a contact as needed by the Call Log. */
+public class ContactInfo {
+
+ public static final ContactInfo EMPTY = new ContactInfo();
+ public Uri lookupUri;
+ /**
+ * Contact lookup key. Note this may be a lookup key for a corp contact, in which case "lookup by
+ * lookup key" doesn't work on the personal profile.
+ */
+ public String lookupKey;
+
+ public String name;
+ public String nameAlternative;
+ public int type;
+ public String label;
+ public String number;
+ public String formattedNumber;
+ /*
+ * ContactInfo.normalizedNumber is a column value returned by PhoneLookup query. By definition,
+ * it's E164 representation.
+ * http://developer.android.com/reference/android/provider/ContactsContract.PhoneLookupColumns.
+ * html#NORMALIZED_NUMBER.
+ *
+ * The fallback value, when PhoneLookup fails or else, should be either null or
+ * PhoneNumberUtils.formatNumberToE164.
+ */
+ public String normalizedNumber;
+ /** The photo for the contact, if available. */
+ public long photoId;
+ /** The high-res photo for the contact, if available. */
+ public Uri photoUri;
+
+ public boolean isBadData;
+ public String objectId;
+ public @UserType long userType;
+ public int sourceType = 0;
+
+ /** @see android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE */
+ public int carrierPresence;
+
+ @Override
+ public int hashCode() {
+ // Uses only name and contactUri to determine hashcode.
+ // This should be sufficient to have a reasonable distribution of hash codes.
+ // Moreover, there should be no two people with the same lookupUri.
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((lookupUri == null) ? 0 : lookupUri.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ContactInfo other = (ContactInfo) obj;
+ if (!UriUtils.areEqual(lookupUri, other.lookupUri)) {
+ return false;
+ }
+ if (!TextUtils.equals(name, other.name)) {
+ return false;
+ }
+ if (!TextUtils.equals(nameAlternative, other.nameAlternative)) {
+ return false;
+ }
+ if (type != other.type) {
+ return false;
+ }
+ if (!TextUtils.equals(label, other.label)) {
+ return false;
+ }
+ if (!TextUtils.equals(number, other.number)) {
+ return false;
+ }
+ if (!TextUtils.equals(formattedNumber, other.formattedNumber)) {
+ return false;
+ }
+ if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) {
+ return false;
+ }
+ if (photoId != other.photoId) {
+ return false;
+ }
+ if (!UriUtils.areEqual(photoUri, other.photoUri)) {
+ return false;
+ }
+ if (!TextUtils.equals(objectId, other.objectId)) {
+ return false;
+ }
+ if (userType != other.userType) {
+ return false;
+ }
+ return carrierPresence == other.carrierPresence;
+ }
+
+ @Override
+ public String toString() {
+ return "ContactInfo{"
+ + "lookupUri="
+ + lookupUri
+ + ", name='"
+ + name
+ + '\''
+ + ", nameAlternative='"
+ + nameAlternative
+ + '\''
+ + ", type="
+ + type
+ + ", label='"
+ + label
+ + '\''
+ + ", number='"
+ + number
+ + '\''
+ + ", formattedNumber='"
+ + formattedNumber
+ + '\''
+ + ", normalizedNumber='"
+ + normalizedNumber
+ + '\''
+ + ", photoId="
+ + photoId
+ + ", photoUri="
+ + photoUri
+ + ", objectId='"
+ + objectId
+ + '\''
+ + ", userType="
+ + userType
+ + ", carrierPresence="
+ + carrierPresence
+ + '}';
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
new file mode 100644
index 000000000..6a5e2e6b4
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2011 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.phonenumbercache;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.PhoneLookup;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.ArrayList;
+import java.util.List;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Utility class to look up the contact information for a given number. */
+// This class uses Java 7 language features, so it must target M+
+@TargetApi(VERSION_CODES.M)
+public class ContactInfoHelper {
+
+ private static final String TAG = ContactInfoHelper.class.getSimpleName();
+
+ private final Context mContext;
+ private final String mCurrentCountryIso;
+ private final CachedNumberLookupService mCachedNumberLookupService;
+
+ public ContactInfoHelper(Context context, String currentCountryIso) {
+ mContext = context;
+ mCurrentCountryIso = currentCountryIso;
+ mCachedNumberLookupService = PhoneNumberCache.get(mContext).getCachedNumberLookupService();
+ }
+
+ /**
+ * Creates a JSON-encoded lookup uri for a unknown number without an associated contact
+ *
+ * @param number - Unknown phone number
+ * @return JSON-encoded URI that can be used to perform a lookup when clicking on the quick
+ * contact card.
+ */
+ private static Uri createTemporaryContactUri(String number) {
+ try {
+ final JSONObject contactRows =
+ new JSONObject()
+ .put(
+ Phone.CONTENT_ITEM_TYPE,
+ new JSONObject().put(Phone.NUMBER, number).put(Phone.TYPE, Phone.TYPE_CUSTOM));
+
+ final String jsonString =
+ new JSONObject()
+ .put(Contacts.DISPLAY_NAME, number)
+ .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.PHONE)
+ .put(Contacts.CONTENT_ITEM_TYPE, contactRows)
+ .toString();
+
+ return Contacts.CONTENT_LOOKUP_URI
+ .buildUpon()
+ .appendPath(Constants.LOOKUP_URI_ENCODED)
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Long.MAX_VALUE))
+ .encodedFragment(jsonString)
+ .build();
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ public static String lookUpDisplayNameAlternative(
+ Context context, String lookupKey, @UserType long userType, @Nullable Long directoryId) {
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
+ if (lookupKey == null || userType == ContactsUtils.USER_TYPE_WORK) {
+ return null;
+ }
+
+ if (directoryId != null) {
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} with work lookup key is not allowed.
+ if (DirectoryCompat.isEnterpriseDirectoryId(directoryId)) {
+ return null;
+ }
+
+ // Skip this to avoid an extra remote network call for alternative name
+ if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
+ return null;
+ }
+ }
+
+ final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
+ Cursor cursor = null;
+ try {
+ cursor =
+ context
+ .getContentResolver()
+ .query(uri, PhoneQuery.DISPLAY_NAME_ALTERNATIVE_PROJECTION, null, null, null);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getString(PhoneQuery.NAME_ALTERNATIVE);
+ }
+ } catch (IllegalArgumentException e) {
+ // Avoid dialer crash when lookup key is not valid
+ Log.e(TAG, "IllegalArgumentException in lookUpDisplayNameAlternative", e);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return null;
+ }
+
+ public static Uri getContactInfoLookupUri(String number) {
+ return getContactInfoLookupUri(number, -1);
+ }
+
+ public static Uri getContactInfoLookupUri(String number, long directoryId) {
+ // Get URI for the number in the PhoneLookup table, with a parameter to indicate whether
+ // the number is a SIP number.
+ Uri uri = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ if (directoryId != -1) {
+ // ENTERPRISE_CONTENT_FILTER_URI in M doesn't support directory lookup
+ uri = PhoneLookup.CONTENT_FILTER_URI;
+ } else {
+ // b/25900607 in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice.
+ number = Uri.encode(number);
+ }
+ }
+ Uri.Builder builder =
+ uri.buildUpon()
+ .appendPath(number)
+ .appendQueryParameter(
+ PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
+ String.valueOf(PhoneNumberHelper.isUriNumber(number)));
+ if (directoryId != -1) {
+ builder.appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns the contact information stored in an entry of the call log.
+ *
+ * @param c A cursor pointing to an entry in the call log.
+ */
+ public static ContactInfo getContactInfo(Cursor c) {
+ ContactInfo info = new ContactInfo();
+ info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
+ info.name = c.getString(CallLogQuery.CACHED_NAME);
+ info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
+ info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
+ String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
+ String postDialDigits =
+ (VERSION.SDK_INT >= VERSION_CODES.N) ? c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
+ info.number =
+ (matchedNumber == null) ? c.getString(CallLogQuery.NUMBER) + postDialDigits : matchedNumber;
+
+ info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
+ info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
+ info.photoUri =
+ UriUtils.nullForNonContactsUri(
+ UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_PHOTO_URI)));
+ info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
+
+ return info;
+ }
+
+ public ContactInfo lookupNumber(String number, String countryIso) {
+ return lookupNumber(number, countryIso, -1);
+ }
+
+ /**
+ * Returns the contact information for the given number.
+ *
+ * <p>If the number does not match any contact, returns a contact info containing only the number
+ * and the formatted number.
+ *
+ * <p>If an error occurs during the lookup, it returns null.
+ *
+ * @param number the number to look up
+ * @param countryIso the country associated with this number
+ * @param directoryId the id of the directory to lookup
+ */
+ @Nullable
+ @SuppressWarnings("ReferenceEquality")
+ public ContactInfo lookupNumber(String number, String countryIso, long directoryId) {
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ ContactInfo info;
+
+ if (PhoneNumberHelper.isUriNumber(number)) {
+ // The number is a SIP address..
+ info = lookupContactFromUri(getContactInfoLookupUri(number, directoryId));
+ if (info == null || info == ContactInfo.EMPTY) {
+ // If lookup failed, check if the "username" of the SIP address is a phone number.
+ String username = PhoneNumberHelper.getUsernameFromUriNumber(number);
+ if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
+ info = queryContactInfoForPhoneNumber(username, countryIso, directoryId);
+ }
+ }
+ } else {
+ // Look for a contact that has the given phone number.
+ info = queryContactInfoForPhoneNumber(number, countryIso, directoryId);
+ }
+
+ final ContactInfo updatedInfo;
+ if (info == null) {
+ // The lookup failed.
+ updatedInfo = null;
+ } else {
+ // If we did not find a matching contact, generate an empty contact info for the number.
+ if (info == ContactInfo.EMPTY) {
+ // Did not find a matching contact.
+ updatedInfo = createEmptyContactInfoForNumber(number, countryIso);
+ } else {
+ updatedInfo = info;
+ }
+ }
+ return updatedInfo;
+ }
+
+ private ContactInfo createEmptyContactInfoForNumber(String number, String countryIso) {
+ ContactInfo contactInfo = new ContactInfo();
+ contactInfo.number = number;
+ contactInfo.formattedNumber = formatPhoneNumber(number, null, countryIso);
+ contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ contactInfo.lookupUri = createTemporaryContactUri(contactInfo.formattedNumber);
+ return contactInfo;
+ }
+
+ /**
+ * Return the contact info object if the remote directory lookup succeeds, otherwise return an
+ * empty contact info for the number.
+ */
+ public ContactInfo lookupNumberInRemoteDirectory(String number, String countryIso) {
+ if (mCachedNumberLookupService != null) {
+ List<Long> remoteDirectories = getRemoteDirectories(mContext);
+ for (long directoryId : remoteDirectories) {
+ ContactInfo contactInfo = lookupNumber(number, countryIso, directoryId);
+ if (hasName(contactInfo)) {
+ return contactInfo;
+ }
+ }
+ }
+ return createEmptyContactInfoForNumber(number, countryIso);
+ }
+
+ public boolean hasName(ContactInfo contactInfo) {
+ return contactInfo != null && !TextUtils.isEmpty(contactInfo.name);
+ }
+
+ private List<Long> getRemoteDirectories(Context context) {
+ List<Long> remoteDirectories = new ArrayList<>();
+ Uri uri =
+ VERSION.SDK_INT >= VERSION_CODES.N
+ ? Directory.ENTERPRISE_CONTENT_URI
+ : Directory.CONTENT_URI;
+ ContentResolver cr = context.getContentResolver();
+ Cursor cursor = cr.query(uri, new String[] {Directory._ID}, null, null, null);
+ int idIndex = cursor.getColumnIndex(Directory._ID);
+ if (cursor == null) {
+ return remoteDirectories;
+ }
+ try {
+ while (cursor.moveToNext()) {
+ long directoryId = cursor.getLong(idIndex);
+ if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
+ remoteDirectories.add(directoryId);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return remoteDirectories;
+ }
+
+ /**
+ * Looks up a contact using the given URI.
+ *
+ * <p>It returns null if an error occurs, {@link ContactInfo#EMPTY} if no matching contact is
+ * found, or the {@link ContactInfo} for the given contact.
+ *
+ * <p>The {@link ContactInfo#formattedNumber} field is always set to {@code null} in the returned
+ * value.
+ */
+ ContactInfo lookupContactFromUri(Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+ if (!PermissionsUtil.hasContactsPermissions(mContext)) {
+ return ContactInfo.EMPTY;
+ }
+
+ Cursor phoneLookupCursor = null;
+ try {
+ String[] projection = PhoneQuery.getPhoneLookupProjection(uri);
+ phoneLookupCursor = mContext.getContentResolver().query(uri, projection, null, null, null);
+ } catch (NullPointerException e) {
+ // Trap NPE from pre-N CP2
+ return null;
+ }
+ if (phoneLookupCursor == null) {
+ return null;
+ }
+
+ try {
+ if (!phoneLookupCursor.moveToFirst()) {
+ return ContactInfo.EMPTY;
+ }
+ String lookupKey = phoneLookupCursor.getString(PhoneQuery.LOOKUP_KEY);
+ ContactInfo contactInfo = createPhoneLookupContactInfo(phoneLookupCursor, lookupKey);
+ fillAdditionalContactInfo(mContext, contactInfo);
+ return contactInfo;
+ } finally {
+ phoneLookupCursor.close();
+ }
+ }
+
+ private ContactInfo createPhoneLookupContactInfo(Cursor phoneLookupCursor, String lookupKey) {
+ ContactInfo info = new ContactInfo();
+ info.lookupKey = lookupKey;
+ info.lookupUri =
+ Contacts.getLookupUri(phoneLookupCursor.getLong(PhoneQuery.PERSON_ID), lookupKey);
+ info.name = phoneLookupCursor.getString(PhoneQuery.NAME);
+ info.type = phoneLookupCursor.getInt(PhoneQuery.PHONE_TYPE);
+ info.label = phoneLookupCursor.getString(PhoneQuery.LABEL);
+ info.number = phoneLookupCursor.getString(PhoneQuery.MATCHED_NUMBER);
+ info.normalizedNumber = phoneLookupCursor.getString(PhoneQuery.NORMALIZED_NUMBER);
+ info.photoId = phoneLookupCursor.getLong(PhoneQuery.PHOTO_ID);
+ info.photoUri = UriUtils.parseUriOrNull(phoneLookupCursor.getString(PhoneQuery.PHOTO_URI));
+ info.formattedNumber = null;
+ info.userType =
+ ContactsUtils.determineUserType(null, phoneLookupCursor.getLong(PhoneQuery.PERSON_ID));
+
+ return info;
+ }
+
+ private void fillAdditionalContactInfo(Context context, ContactInfo contactInfo) {
+ if (contactInfo.number == null) {
+ return;
+ }
+ Uri uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(contactInfo.number));
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(uri, PhoneQuery.ADDITIONAL_CONTACT_INFO_PROJECTION, null, null, null)) {
+ if (cursor == null || !cursor.moveToFirst()) {
+ return;
+ }
+ contactInfo.nameAlternative =
+ cursor.getString(PhoneQuery.ADDITIONAL_CONTACT_INFO_DISPLAY_NAME_ALTERNATIVE);
+ contactInfo.carrierPresence =
+ cursor.getInt(PhoneQuery.ADDITIONAL_CONTACT_INFO_CARRIER_PRESENCE);
+ }
+ }
+
+ /**
+ * Determines the contact information for the given phone number.
+ *
+ * <p>It returns the contact info if found.
+ *
+ * <p>If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
+ *
+ * <p>If the lookup fails for some other reason, it returns null.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private ContactInfo queryContactInfoForPhoneNumber(
+ String number, String countryIso, long directoryId) {
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ ContactInfo info = lookupContactFromUri(getContactInfoLookupUri(number, directoryId));
+ if (info != null && info != ContactInfo.EMPTY) {
+ info.formattedNumber = formatPhoneNumber(number, null, countryIso);
+ } else if (mCachedNumberLookupService != null) {
+ CachedContactInfo cacheInfo =
+ mCachedNumberLookupService.lookupCachedContactFromNumber(mContext, number);
+ if (cacheInfo != null) {
+ info = cacheInfo.getContactInfo().isBadData ? null : cacheInfo.getContactInfo();
+ } else {
+ info = null;
+ }
+ }
+ return info;
+ }
+
+ /**
+ * Format the given phone number
+ *
+ * @param number the number to be formatted.
+ * @param normalizedNumber the normalized number of the given number.
+ * @param countryIso the ISO 3166-1 two letters country code, the country's convention will be
+ * used to format the number if the normalized phone is null.
+ * @return the formatted number, or the given number if it was formatted.
+ */
+ private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
+ if (TextUtils.isEmpty(number)) {
+ return "";
+ }
+ // If "number" is really a SIP address, don't try to do any formatting at all.
+ if (PhoneNumberHelper.isUriNumber(number)) {
+ return number;
+ }
+ if (TextUtils.isEmpty(countryIso)) {
+ countryIso = mCurrentCountryIso;
+ }
+ return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
+ }
+
+ /**
+ * Stores differences between the updated contact info and the current call log contact info.
+ *
+ * @param number The number of the contact.
+ * @param countryIso The country associated with this number.
+ * @param updatedInfo The updated contact info.
+ * @param callLogInfo The call log entry's current contact info.
+ */
+ public void updateCallLogContactInfo(
+ String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo) {
+ if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.WRITE_CALL_LOG)) {
+ return;
+ }
+
+ final ContentValues values = new ContentValues();
+ boolean needsUpdate = false;
+
+ if (callLogInfo != null) {
+ if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
+ values.put(Calls.CACHED_NAME, updatedInfo.name);
+ needsUpdate = true;
+ }
+
+ if (updatedInfo.type != callLogInfo.type) {
+ values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+ needsUpdate = true;
+ }
+
+ if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
+ values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+ needsUpdate = true;
+ }
+
+ if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
+ values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+ needsUpdate = true;
+ }
+
+ // Only replace the normalized number if the new updated normalized number isn't empty.
+ if (!TextUtils.isEmpty(updatedInfo.normalizedNumber)
+ && !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
+ values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+ needsUpdate = true;
+ }
+
+ if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
+ values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+ needsUpdate = true;
+ }
+
+ if (updatedInfo.photoId != callLogInfo.photoId) {
+ values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+ needsUpdate = true;
+ }
+
+ final Uri updatedPhotoUriContactsOnly = UriUtils.nullForNonContactsUri(updatedInfo.photoUri);
+ if (!UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) {
+ values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(updatedPhotoUriContactsOnly));
+ needsUpdate = true;
+ }
+
+ if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
+ values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+ needsUpdate = true;
+ }
+ } else {
+ // No previous values, store all of them.
+ values.put(Calls.CACHED_NAME, updatedInfo.name);
+ values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+ values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+ values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+ values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+ values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+ values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+ values.put(
+ Calls.CACHED_PHOTO_URI,
+ UriUtils.uriToString(UriUtils.nullForNonContactsUri(updatedInfo.photoUri)));
+ values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+ needsUpdate = true;
+ }
+
+ if (!needsUpdate) {
+ return;
+ }
+
+ try {
+ if (countryIso == null) {
+ mContext
+ .getContentResolver()
+ .update(
+ TelecomUtil.getCallLogUri(mContext),
+ values,
+ Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
+ new String[] {number});
+ } else {
+ mContext
+ .getContentResolver()
+ .update(
+ TelecomUtil.getCallLogUri(mContext),
+ values,
+ Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
+ new String[] {number, countryIso});
+ }
+ } catch (SQLiteFullException e) {
+ Log.e(TAG, "Unable to update contact info in call log db", e);
+ }
+ }
+
+ public void updateCachedNumberLookupService(ContactInfo updatedInfo) {
+ if (mCachedNumberLookupService != null) {
+ if (hasName(updatedInfo)) {
+ CachedContactInfo cachedContactInfo =
+ mCachedNumberLookupService.buildCachedContactInfo(updatedInfo);
+ mCachedNumberLookupService.addContact(mContext, cachedContactInfo);
+ }
+ }
+ }
+
+ /**
+ * Given a contact's sourceType, return true if the contact is a business
+ *
+ * @param sourceType sourceType of the contact. This is usually populated by {@link
+ * #mCachedNumberLookupService}.
+ */
+ public boolean isBusiness(int sourceType) {
+ return mCachedNumberLookupService != null && mCachedNumberLookupService.isBusiness(sourceType);
+ }
+
+ /**
+ * This function looks at a contact's source and determines if the user can mark caller ids from
+ * this source as invalid.
+ *
+ * @param sourceType The source type to be checked
+ * @param objectId The ID of the Contact object.
+ * @return true if contacts from this source can be marked with an invalid caller id
+ */
+ public boolean canReportAsInvalid(int sourceType, String objectId) {
+ return mCachedNumberLookupService != null
+ && mCachedNumberLookupService.canReportAsInvalid(sourceType, objectId);
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java b/java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java
new file mode 100644
index 000000000..74175e8ba
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 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.phonenumbercache;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.PhoneLookup;
+
+public final class PhoneLookupUtil {
+
+ private PhoneLookupUtil() {}
+
+ /** @return the column name that stores contact id for phone lookup query. */
+ public static String getContactIdColumnNameForUri(Uri phoneLookupUri) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return PhoneLookup.CONTACT_ID;
+ }
+ // In pre-N, contact id is stored in {@link PhoneLookup#_ID} in non-sip query.
+ boolean isSip =
+ phoneLookupUri.getBooleanQueryParameter(
+ ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
+ return (isSip) ? PhoneLookup.CONTACT_ID : ContactsContract.PhoneLookup._ID;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCache.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCache.java
new file mode 100644
index 000000000..aefa544cb
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCache.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 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.phonenumbercache;
+
+import android.content.Context;
+import java.util.Objects;
+
+/** Accessor for the phone number cache bindings. */
+public class PhoneNumberCache {
+
+ private static PhoneNumberCacheBindings phoneNumberCacheBindings;
+
+ private PhoneNumberCache() {}
+
+ public static PhoneNumberCacheBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (phoneNumberCacheBindings != null) {
+ return phoneNumberCacheBindings;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof PhoneNumberCacheBindingsFactory) {
+ phoneNumberCacheBindings =
+ ((PhoneNumberCacheBindingsFactory) application).newPhoneNumberCacheBindings();
+ }
+
+ if (phoneNumberCacheBindings == null) {
+ phoneNumberCacheBindings = new PhoneNumberCacheBindingsStub();
+ }
+ return phoneNumberCacheBindings;
+ }
+
+ public static void setForTesting(PhoneNumberCacheBindings phoneNumberCacheBindings) {
+ PhoneNumberCache.phoneNumberCacheBindings = phoneNumberCacheBindings;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java
new file mode 100644
index 000000000..6e3ed9d06
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016 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.phonenumbercache;
+
+import android.support.annotation.Nullable;
+
+/** Allows the container application provide a number look up service. */
+public interface PhoneNumberCacheBindings {
+
+ @Nullable
+ CachedNumberLookupService getCachedNumberLookupService();
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java
new file mode 100644
index 000000000..3552529ba
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016 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.phonenumbercache;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows this module to get
+ * references to the PhoneNumberCacheBindings.
+ */
+public interface PhoneNumberCacheBindingsFactory {
+
+ PhoneNumberCacheBindings newPhoneNumberCacheBindings();
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java
new file mode 100644
index 000000000..c7fb97807
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 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.phonenumbercache;
+
+import android.support.annotation.Nullable;
+
+/** Default implementation of PhoneNumberCacheBindings. */
+public class PhoneNumberCacheBindingsStub implements PhoneNumberCacheBindings {
+
+ @Override
+ @Nullable
+ public CachedNumberLookupService getCachedNumberLookupService() {
+ return null;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneQuery.java b/java/com/android/dialer/phonenumbercache/PhoneQuery.java
new file mode 100644
index 000000000..5ddd5f846
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneQuery.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2011 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.phonenumbercache;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.PhoneLookup;
+
+/** The queries to look up the {@link ContactInfo} for a given number in the Call Log. */
+final class PhoneQuery {
+
+ public static final int PERSON_ID = 0;
+ public static final int NAME = 1;
+ public static final int PHONE_TYPE = 2;
+ public static final int LABEL = 3;
+ public static final int MATCHED_NUMBER = 4;
+ public static final int NORMALIZED_NUMBER = 5;
+ public static final int PHOTO_ID = 6;
+ public static final int LOOKUP_KEY = 7;
+ public static final int PHOTO_URI = 8;
+ /** Projection to look up a contact's DISPLAY_NAME_ALTERNATIVE */
+ public static final String[] DISPLAY_NAME_ALTERNATIVE_PROJECTION =
+ new String[] {
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ };
+
+ public static final int NAME_ALTERNATIVE = 0;
+
+ public static final String[] ADDITIONAL_CONTACT_INFO_PROJECTION =
+ new String[] {Phone.DISPLAY_NAME_ALTERNATIVE, Phone.CARRIER_PRESENCE};
+ public static final int ADDITIONAL_CONTACT_INFO_DISPLAY_NAME_ALTERNATIVE = 0;
+ public static final int ADDITIONAL_CONTACT_INFO_CARRIER_PRESENCE = 1;
+
+ /**
+ * Projection to look up the ContactInfo. Does not include DISPLAY_NAME_ALTERNATIVE as that column
+ * isn't available in ContactsCommon.PhoneLookup. We should always use this projection starting
+ * from NYC onward.
+ */
+ private static final String[] PHONE_LOOKUP_PROJECTION =
+ new String[] {
+ PhoneLookup.CONTACT_ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.TYPE,
+ PhoneLookup.LABEL,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER,
+ PhoneLookup.PHOTO_ID,
+ PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.PHOTO_URI
+ };
+ /**
+ * Similar to {@link PHONE_LOOKUP_PROJECTION}. In pre-N, contact id is stored in {@link
+ * PhoneLookup#_ID} in non-sip query.
+ */
+ private static final String[] BACKWARD_COMPATIBLE_NON_SIP_PHONE_LOOKUP_PROJECTION =
+ new String[] {
+ PhoneLookup._ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.TYPE,
+ PhoneLookup.LABEL,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER,
+ PhoneLookup.PHOTO_ID,
+ PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.PHOTO_URI
+ };
+
+ public static String[] getPhoneLookupProjection(Uri phoneLookupUri) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return PHONE_LOOKUP_PROJECTION;
+ }
+ // Pre-N
+ boolean isSip =
+ phoneLookupUri.getBooleanQueryParameter(
+ ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
+ return (isSip) ? PHONE_LOOKUP_PROJECTION : BACKWARD_COMPATIBLE_NON_SIP_PHONE_LOOKUP_PROJECTION;
+ }
+}