diff options
Diffstat (limited to 'java/com/android/dialer/phonenumbercache')
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; + } +} |