summaryrefslogtreecommitdiff
path: root/java/com/android/incallui/ContactInfoCache.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/incallui/ContactInfoCache.java')
-rw-r--r--java/com/android/incallui/ContactInfoCache.java759
1 files changed, 759 insertions, 0 deletions
diff --git a/java/com/android/incallui/ContactInfoCache.java b/java/com/android/incallui/ContactInfoCache.java
new file mode 100644
index 000000000..4d4d94a17
--- /dev/null
+++ b/java/com/android/incallui/ContactInfoCache.java
@@ -0,0 +1,759 @@
+/*
+ * Copyright (C) 2013 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.incallui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+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.DisplayNameSources;
+import android.support.annotation.AnyThread;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.support.v4.os.UserManagerCompat;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import com.android.contacts.common.ContactsUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.logging.nano.ContactLookupResult;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.PhoneNumberCache;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.MoreStrings;
+import com.android.incallui.CallerInfoAsyncQuery.OnQueryCompleteListener;
+import com.android.incallui.ContactsAsyncHelper.OnImageLoadCompleteListener;
+import com.android.incallui.bindings.PhoneNumberService;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.incall.protocol.ContactPhotoType;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Class responsible for querying Contact Information for DialerCall objects. Can perform
+ * asynchronous requests to the Contact Provider for information as well as respond synchronously
+ * for any data that it currently has cached from previous queries. This class always gets called
+ * from the UI thread so it does not need thread protection.
+ */
+public class ContactInfoCache implements OnImageLoadCompleteListener {
+
+ private static final String TAG = ContactInfoCache.class.getSimpleName();
+ private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
+ private static ContactInfoCache sCache = null;
+ private final Context mContext;
+ private final PhoneNumberService mPhoneNumberService;
+ // Cache info map needs to be thread-safe since it could be modified by both main thread and
+ // worker thread.
+ private final Map<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
+ private final Map<String, Set<ContactInfoCacheCallback>> mCallBacks = new ArrayMap<>();
+ private Drawable mDefaultContactPhotoDrawable;
+ private Drawable mConferencePhotoDrawable;
+
+ private ContactInfoCache(Context context) {
+ mContext = context;
+ mPhoneNumberService = Bindings.get(context).newPhoneNumberService(context);
+ }
+
+ public static synchronized ContactInfoCache getInstance(Context mContext) {
+ if (sCache == null) {
+ sCache = new ContactInfoCache(mContext.getApplicationContext());
+ }
+ return sCache;
+ }
+
+ public static ContactCacheEntry buildCacheEntryFromCall(
+ Context context, DialerCall call, boolean isIncoming) {
+ final ContactCacheEntry entry = new ContactCacheEntry();
+
+ // TODO: get rid of caller info.
+ final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, call);
+ ContactInfoCache.populateCacheEntry(
+ context, info, entry, call.getNumberPresentation(), isIncoming);
+ return entry;
+ }
+
+ /** Populate a cache entry from a call (which got converted into a caller info). */
+ public static void populateCacheEntry(
+ @NonNull Context context,
+ @NonNull CallerInfo info,
+ @NonNull ContactCacheEntry cce,
+ int presentation,
+ boolean isIncoming) {
+ Objects.requireNonNull(info);
+ String displayName = null;
+ String displayNumber = null;
+ String displayLocation = null;
+ String label = null;
+ boolean isSipCall = false;
+
+ // It appears that there is a small change in behaviour with the
+ // PhoneUtils' startGetCallerInfo whereby if we query with an
+ // empty number, we will get a valid CallerInfo object, but with
+ // fields that are all null, and the isTemporary boolean input
+ // parameter as true.
+
+ // In the past, we would see a NULL callerinfo object, but this
+ // ends up causing null pointer exceptions elsewhere down the
+ // line in other cases, so we need to make this fix instead. It
+ // appears that this was the ONLY call to PhoneUtils
+ // .getCallerInfo() that relied on a NULL CallerInfo to indicate
+ // an unknown contact.
+
+ // Currently, info.phoneNumber may actually be a SIP address, and
+ // if so, it might sometimes include the "sip:" prefix. That
+ // prefix isn't really useful to the user, though, so strip it off
+ // if present. (For any other URI scheme, though, leave the
+ // prefix alone.)
+ // TODO: It would be cleaner for CallerInfo to explicitly support
+ // SIP addresses instead of overloading the "phoneNumber" field.
+ // Then we could remove this hack, and instead ask the CallerInfo
+ // for a "user visible" form of the SIP address.
+ String number = info.phoneNumber;
+
+ if (!TextUtils.isEmpty(number)) {
+ isSipCall = PhoneNumberHelper.isUriNumber(number);
+ if (number.startsWith("sip:")) {
+ number = number.substring(4);
+ }
+ }
+
+ if (TextUtils.isEmpty(info.name)) {
+ // No valid "name" in the CallerInfo, so fall back to
+ // something else.
+ // (Typically, we promote the phone number up to the "name" slot
+ // onscreen, and possibly display a descriptive string in the
+ // "number" slot.)
+ if (TextUtils.isEmpty(number)) {
+ // No name *or* number! Display a generic "unknown" string
+ // (or potentially some other default based on the presentation.)
+ displayName = getPresentationString(context, presentation, info.callSubject);
+ Log.d(TAG, " ==> no name *or* number! displayName = " + displayName);
+ } else if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
+ // This case should never happen since the network should never send a phone #
+ // AND a restricted presentation. However we leave it here in case of weird
+ // network behavior
+ displayName = getPresentationString(context, presentation, info.callSubject);
+ Log.d(TAG, " ==> presentation not allowed! displayName = " + displayName);
+ } else if (!TextUtils.isEmpty(info.cnapName)) {
+ // No name, but we do have a valid CNAP name, so use that.
+ displayName = info.cnapName;
+ info.name = info.cnapName;
+ displayNumber = PhoneNumberHelper.formatNumber(number, context);
+ Log.d(
+ TAG,
+ " ==> cnapName available: displayName '"
+ + displayName
+ + "', displayNumber '"
+ + displayNumber
+ + "'");
+ } else {
+ // No name; all we have is a number. This is the typical
+ // case when an incoming call doesn't match any contact,
+ // or if you manually dial an outgoing number using the
+ // dialpad.
+ displayNumber = PhoneNumberHelper.formatNumber(number, context);
+
+ // Display a geographical description string if available
+ // (but only for incoming calls.)
+ if (isIncoming) {
+ // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
+ // query to only do the geoDescription lookup in the first
+ // place for incoming calls.
+ displayLocation = info.geoDescription; // may be null
+ Log.d(TAG, "Geodescrption: " + info.geoDescription);
+ }
+
+ Log.d(
+ TAG,
+ " ==> no name; falling back to number:"
+ + " displayNumber '"
+ + Log.pii(displayNumber)
+ + "', displayLocation '"
+ + displayLocation
+ + "'");
+ }
+ } else {
+ // We do have a valid "name" in the CallerInfo. Display that
+ // in the "name" slot, and the phone number in the "number" slot.
+ if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
+ // This case should never happen since the network should never send a name
+ // AND a restricted presentation. However we leave it here in case of weird
+ // network behavior
+ displayName = getPresentationString(context, presentation, info.callSubject);
+ Log.d(
+ TAG,
+ " ==> valid name, but presentation not allowed!" + " displayName = " + displayName);
+ } else {
+ // Causes cce.namePrimary to be set as info.name below. CallCardPresenter will
+ // later determine whether to use the name or nameAlternative when presenting
+ displayName = info.name;
+ cce.nameAlternative = info.nameAlternative;
+ displayNumber = PhoneNumberHelper.formatNumber(number, context);
+ label = info.phoneLabel;
+ Log.d(
+ TAG,
+ " ==> name is present in CallerInfo: displayName '"
+ + displayName
+ + "', displayNumber '"
+ + displayNumber
+ + "'");
+ }
+ }
+
+ cce.namePrimary = displayName;
+ cce.number = displayNumber;
+ cce.location = displayLocation;
+ cce.label = label;
+ cce.isSipCall = isSipCall;
+ cce.userType = info.userType;
+
+ if (info.contactExists) {
+ cce.contactLookupResult = ContactLookupResult.Type.LOCAL_CONTACT;
+ }
+ }
+
+ /** Gets name strings based on some special presentation modes and the associated custom label. */
+ private static String getPresentationString(
+ Context context, int presentation, String customLabel) {
+ String name = context.getString(R.string.unknown);
+ if (!TextUtils.isEmpty(customLabel)
+ && ((presentation == TelecomManager.PRESENTATION_UNKNOWN)
+ || (presentation == TelecomManager.PRESENTATION_RESTRICTED))) {
+ name = customLabel;
+ return name;
+ } else {
+ if (presentation == TelecomManager.PRESENTATION_RESTRICTED) {
+ name = PhoneNumberHelper.getDisplayNameForRestrictedNumber(context).toString();
+ } else if (presentation == TelecomManager.PRESENTATION_PAYPHONE) {
+ name = context.getString(R.string.payphone);
+ }
+ }
+ return name;
+ }
+
+ public ContactCacheEntry getInfo(String callId) {
+ return mInfoMap.get(callId);
+ }
+
+ public void maybeInsertCnapInformationIntoCache(
+ Context context, final DialerCall call, final CallerInfo info) {
+ final CachedNumberLookupService cachedNumberLookupService =
+ PhoneNumberCache.get(context).getCachedNumberLookupService();
+ if (!UserManagerCompat.isUserUnlocked(context)) {
+ Log.i(TAG, "User locked, not inserting cnap info into cache");
+ return;
+ }
+ if (cachedNumberLookupService == null
+ || TextUtils.isEmpty(info.cnapName)
+ || mInfoMap.get(call.getId()) != null) {
+ return;
+ }
+ final Context applicationContext = context.getApplicationContext();
+ Log.i(TAG, "Found contact with CNAP name - inserting into cache");
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ ContactInfo contactInfo = new ContactInfo();
+ CachedContactInfo cacheInfo = cachedNumberLookupService.buildCachedContactInfo(contactInfo);
+ cacheInfo.setSource(CachedContactInfo.SOURCE_TYPE_CNAP, "CNAP", 0);
+ contactInfo.name = info.cnapName;
+ contactInfo.number = call.getNumber();
+ contactInfo.type = ContactsContract.CommonDataKinds.Phone.TYPE_MAIN;
+ try {
+ final JSONObject contactRows =
+ new JSONObject()
+ .put(
+ Phone.CONTENT_ITEM_TYPE,
+ new JSONObject()
+ .put(Phone.NUMBER, contactInfo.number)
+ .put(Phone.TYPE, Phone.TYPE_MAIN));
+ final String jsonString =
+ new JSONObject()
+ .put(Contacts.DISPLAY_NAME, contactInfo.name)
+ .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME)
+ .put(Contacts.CONTENT_ITEM_TYPE, contactRows)
+ .toString();
+ cacheInfo.setLookupKey(jsonString);
+ } catch (JSONException e) {
+ Log.w(TAG, "Creation of lookup key failed when caching CNAP information");
+ }
+ cachedNumberLookupService.addContact(applicationContext, cacheInfo);
+ return null;
+ }
+ }.execute();
+ }
+
+ /**
+ * Requests contact data for the DialerCall object passed in. Returns the data through callback.
+ * If callback is null, no response is made, however the query is still performed and cached.
+ *
+ * @param callback The function to call back when the call is found. Can be null.
+ */
+ @MainThread
+ public void findInfo(
+ @NonNull final DialerCall call,
+ final boolean isIncoming,
+ @NonNull ContactInfoCacheCallback callback) {
+ Assert.isMainThread();
+ Objects.requireNonNull(callback);
+
+ final String callId = call.getId();
+ final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
+ Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+
+ // If we have a previously obtained intermediate result return that now
+ if (cacheEntry != null) {
+ Log.d(
+ TAG,
+ "Contact lookup. In memory cache hit; lookup "
+ + (callBacks == null ? "complete" : "still running"));
+ callback.onContactInfoComplete(callId, cacheEntry);
+ // If no other callbacks are in flight, we're done.
+ if (callBacks == null) {
+ return;
+ }
+ }
+
+ // If the entry already exists, add callback
+ if (callBacks != null) {
+ callBacks.add(callback);
+ return;
+ }
+ Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
+ // New lookup
+ callBacks = new ArraySet<>();
+ callBacks.add(callback);
+ mCallBacks.put(callId, callBacks);
+
+ /**
+ * Performs a query for caller information. Save any immediate data we get from the query. An
+ * asynchronous query may also be made for any data that we do not already have. Some queries,
+ * such as those for voicemail and emergency call information, will not perform an additional
+ * asynchronous query.
+ */
+ final CallerInfo callerInfo =
+ CallerInfoUtils.getCallerInfoForCall(
+ mContext,
+ call,
+ new DialerCallCookieWrapper(callId, call.getNumberPresentation()),
+ new FindInfoCallback(isIncoming));
+
+ updateCallerInfoInCacheOnAnyThread(
+ callId, call.getNumberPresentation(), callerInfo, isIncoming, false);
+ sendInfoNotifications(callId, mInfoMap.get(callId));
+ }
+
+ @AnyThread
+ private void updateCallerInfoInCacheOnAnyThread(
+ String callId,
+ int numberPresentation,
+ CallerInfo callerInfo,
+ boolean isIncoming,
+ boolean didLocalLookup) {
+ int presentationMode = numberPresentation;
+ if (callerInfo.contactExists
+ || callerInfo.isEmergencyNumber()
+ || callerInfo.isVoiceMailNumber()) {
+ presentationMode = TelecomManager.PRESENTATION_ALLOWED;
+ }
+
+ synchronized (mInfoMap) {
+ ContactCacheEntry cacheEntry = mInfoMap.get(callId);
+ // Ensure we always have a cacheEntry. Replace the existing entry if
+ // it has no name or if we found a local contact.
+ if (cacheEntry == null
+ || TextUtils.isEmpty(cacheEntry.namePrimary)
+ || callerInfo.contactExists) {
+ cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
+ mInfoMap.put(callId, cacheEntry);
+ }
+ if (didLocalLookup) {
+ // Before issuing a request for more data from other services, we only check that the
+ // contact wasn't found in the local DB. We don't check the if the cache entry already
+ // has a name because we allow overriding cnap data with data from other services.
+ if (!callerInfo.contactExists && mPhoneNumberService != null) {
+ Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
+ final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId);
+ mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
+ } else if (cacheEntry.displayPhotoUri != null) {
+ Log.d(TAG, "Contact lookup. Local contact found, starting image load");
+ // Load the image with a callback to update the image state.
+ // When the load is finished, onImageLoadComplete() will be called.
+ cacheEntry.hasPhotoToLoad = true;
+ ContactsAsyncHelper.startObtainPhotoAsync(
+ TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
+ mContext,
+ cacheEntry.displayPhotoUri,
+ ContactInfoCache.this,
+ callId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. Update contact photo
+ * when image is loaded in worker thread.
+ */
+ @WorkerThread
+ @Override
+ public void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+ Assert.isWorkerThread();
+ loadImage(photo, photoIcon, cookie);
+ }
+
+ private void loadImage(Drawable photo, Bitmap photoIcon, Object cookie) {
+ Log.d(this, "Image load complete with context: ", mContext);
+ // TODO: may be nice to update the image view again once the newer one
+ // is available on contacts database.
+ String callId = (String) cookie;
+ ContactCacheEntry entry = mInfoMap.get(callId);
+
+ if (entry == null) {
+ Log.e(this, "Image Load received for empty search entry.");
+ clearCallbacks(callId);
+ return;
+ }
+
+ Log.d(this, "setting photo for entry: ", entry);
+
+ // Conference call icons are being handled in CallCardPresenter.
+ if (photo != null) {
+ Log.v(this, "direct drawable: ", photo);
+ entry.photo = photo;
+ entry.photoType = ContactPhotoType.CONTACT;
+ } else if (photoIcon != null) {
+ Log.v(this, "photo icon: ", photoIcon);
+ entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
+ entry.photoType = ContactPhotoType.CONTACT;
+ } else {
+ Log.v(this, "unknown photo");
+ entry.photo = null;
+ entry.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
+ }
+ }
+
+ /**
+ * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. make sure that the
+ * call state is reflected after the image is loaded.
+ */
+ @MainThread
+ @Override
+ public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+ Assert.isMainThread();
+ String callId = (String) cookie;
+ ContactCacheEntry entry = mInfoMap.get(callId);
+ sendImageNotifications(callId, entry);
+
+ clearCallbacks(callId);
+ }
+
+ /** Blows away the stored cache values. */
+ public void clearCache() {
+ mInfoMap.clear();
+ mCallBacks.clear();
+ }
+
+ private ContactCacheEntry buildEntry(
+ Context context, CallerInfo info, int presentation, boolean isIncoming) {
+ final ContactCacheEntry cce = new ContactCacheEntry();
+ populateCacheEntry(context, info, cce, presentation, isIncoming);
+
+ // This will only be true for emergency numbers
+ if (info.photoResource != 0) {
+ cce.photo = context.getResources().getDrawable(info.photoResource);
+ } else if (info.isCachedPhotoCurrent) {
+ if (info.cachedPhoto != null) {
+ cce.photo = info.cachedPhoto;
+ cce.photoType = ContactPhotoType.CONTACT;
+ } else {
+ cce.photo = getDefaultContactPhotoDrawable();
+ cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
+ }
+ } else if (info.contactDisplayPhotoUri == null) {
+ cce.photo = getDefaultContactPhotoDrawable();
+ cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
+ } else {
+ cce.displayPhotoUri = info.contactDisplayPhotoUri;
+ cce.photo = null;
+ }
+
+ // Support any contact id in N because QuickContacts in N starts supporting enterprise
+ // contact id
+ if (info.lookupKeyOrNull != null
+ && (VERSION.SDK_INT >= VERSION_CODES.N || info.contactIdOrZero != 0)) {
+ cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull);
+ } else {
+ Log.v(TAG, "lookup key is null or contact ID is 0 on M. Don't create a lookup uri.");
+ cce.lookupUri = null;
+ }
+
+ cce.lookupKey = info.lookupKeyOrNull;
+ cce.contactRingtoneUri = info.contactRingtoneUri;
+ if (cce.contactRingtoneUri == null || Uri.EMPTY.equals(cce.contactRingtoneUri)) {
+ cce.contactRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
+ }
+
+ return cce;
+ }
+
+ /** Sends the updated information to call the callbacks for the entry. */
+ private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
+ final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+ if (callBacks != null) {
+ for (ContactInfoCacheCallback callBack : callBacks) {
+ callBack.onContactInfoComplete(callId, entry);
+ }
+ }
+ }
+
+ private void sendImageNotifications(String callId, ContactCacheEntry entry) {
+ final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+ if (callBacks != null && entry.photo != null) {
+ for (ContactInfoCacheCallback callBack : callBacks) {
+ callBack.onImageLoadComplete(callId, entry);
+ }
+ }
+ }
+
+ private void clearCallbacks(String callId) {
+ mCallBacks.remove(callId);
+ }
+
+ public Drawable getDefaultContactPhotoDrawable() {
+ if (mDefaultContactPhotoDrawable == null) {
+ mDefaultContactPhotoDrawable =
+ mContext.getResources().getDrawable(R.drawable.img_no_image_automirrored);
+ }
+ return mDefaultContactPhotoDrawable;
+ }
+
+ public Drawable getConferenceDrawable() {
+ if (mConferencePhotoDrawable == null) {
+ mConferencePhotoDrawable =
+ mContext.getResources().getDrawable(R.drawable.img_conference_automirrored);
+ }
+ return mConferencePhotoDrawable;
+ }
+
+ /** Callback interface for the contact query. */
+ public interface ContactInfoCacheCallback {
+
+ void onContactInfoComplete(String callId, ContactCacheEntry entry);
+
+ void onImageLoadComplete(String callId, ContactCacheEntry entry);
+ }
+
+ /** This is cached contact info, which should be the ONLY info used by UI. */
+ public static class ContactCacheEntry {
+
+ public String namePrimary;
+ public String nameAlternative;
+ public String number;
+ public String location;
+ public String label;
+ public Drawable photo;
+ @ContactPhotoType public int photoType;
+ public boolean isSipCall;
+ // Note in cache entry whether this is a pending async loading action to know whether to
+ // wait for its callback or not.
+ public boolean hasPhotoToLoad;
+ /** This will be used for the "view" notification. */
+ public Uri contactUri;
+ /** Either a display photo or a thumbnail URI. */
+ public Uri displayPhotoUri;
+
+ public Uri lookupUri; // Sent to NotificationMananger
+ public String lookupKey;
+ public int contactLookupResult = ContactLookupResult.Type.NOT_FOUND;
+ public long userType = ContactsUtils.USER_TYPE_CURRENT;
+ public Uri contactRingtoneUri;
+
+ @Override
+ public String toString() {
+ return "ContactCacheEntry{"
+ + "name='"
+ + MoreStrings.toSafeString(namePrimary)
+ + '\''
+ + ", nameAlternative='"
+ + MoreStrings.toSafeString(nameAlternative)
+ + '\''
+ + ", number='"
+ + MoreStrings.toSafeString(number)
+ + '\''
+ + ", location='"
+ + MoreStrings.toSafeString(location)
+ + '\''
+ + ", label='"
+ + label
+ + '\''
+ + ", photo="
+ + photo
+ + ", isSipCall="
+ + isSipCall
+ + ", contactUri="
+ + contactUri
+ + ", displayPhotoUri="
+ + displayPhotoUri
+ + ", contactLookupResult="
+ + contactLookupResult
+ + ", userType="
+ + userType
+ + ", contactRingtoneUri="
+ + contactRingtoneUri
+ + '}';
+ }
+ }
+
+ private static final class DialerCallCookieWrapper {
+ public final String callId;
+ public final int numberPresentation;
+
+ public DialerCallCookieWrapper(String callId, int numberPresentation) {
+ this.callId = callId;
+ this.numberPresentation = numberPresentation;
+ }
+ }
+
+ private class FindInfoCallback implements OnQueryCompleteListener {
+
+ private final boolean mIsIncoming;
+
+ public FindInfoCallback(boolean isIncoming) {
+ mIsIncoming = isIncoming;
+ }
+
+ @Override
+ public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ Assert.isWorkerThread();
+ DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
+ updateCallerInfoInCacheOnAnyThread(cw.callId, cw.numberPresentation, ci, mIsIncoming, true);
+ }
+
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
+ Assert.isMainThread();
+ DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
+ String callId = cw.callId;
+ ContactCacheEntry cacheEntry = mInfoMap.get(callId);
+ // This may happen only when InCallPresenter attempt to cleanup.
+ if (cacheEntry == null) {
+ Log.w(TAG, "Contact lookup done, but cache entry is not found.");
+ clearCallbacks(callId);
+ return;
+ }
+ sendInfoNotifications(callId, cacheEntry);
+ if (!cacheEntry.hasPhotoToLoad) {
+ if (callerInfo.contactExists) {
+ Log.d(TAG, "Contact lookup done. Local contact found, no image.");
+ } else {
+ Log.d(
+ TAG,
+ "Contact lookup done. Local contact not found and"
+ + " no remote lookup service available.");
+ }
+ clearCallbacks(callId);
+ }
+ }
+ }
+
+ class PhoneNumberServiceListener
+ implements PhoneNumberService.NumberLookupListener, PhoneNumberService.ImageLookupListener {
+
+ private final String mCallId;
+
+ PhoneNumberServiceListener(String callId) {
+ mCallId = callId;
+ }
+
+ @Override
+ public void onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info) {
+ // If we got a miss, this is the end of the lookup pipeline,
+ // so clear the callbacks and return.
+ if (info == null) {
+ Log.d(TAG, "Contact lookup done. Remote contact not found.");
+ clearCallbacks(mCallId);
+ return;
+ }
+
+ ContactCacheEntry entry = new ContactCacheEntry();
+ entry.namePrimary = info.getDisplayName();
+ entry.number = info.getNumber();
+ entry.contactLookupResult = info.getLookupSource();
+ final int type = info.getPhoneType();
+ final String label = info.getPhoneLabel();
+ if (type == Phone.TYPE_CUSTOM) {
+ entry.label = label;
+ } else {
+ final CharSequence typeStr = Phone.getTypeLabel(mContext.getResources(), type, label);
+ entry.label = typeStr == null ? null : typeStr.toString();
+ }
+ synchronized (mInfoMap) {
+ final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
+ if (oldEntry != null) {
+ // Location is only obtained from local lookup so persist
+ // the value for remote lookups. Once we have a name this
+ // field is no longer used; it is persisted here in case
+ // the UI is ever changed to use it.
+ entry.location = oldEntry.location;
+ // Contact specific ringtone is obtained from local lookup.
+ entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
+ }
+
+ // If no image and it's a business, switch to using the default business avatar.
+ if (info.getImageUrl() == null && info.isBusiness()) {
+ Log.d(TAG, "Business has no image. Using default.");
+ entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
+ entry.photoType = ContactPhotoType.BUSINESS;
+ }
+
+ mInfoMap.put(mCallId, entry);
+ }
+ sendInfoNotifications(mCallId, entry);
+
+ entry.hasPhotoToLoad = info.getImageUrl() != null;
+
+ // If there is no image then we should not expect another callback.
+ if (!entry.hasPhotoToLoad) {
+ // We're done, so clear callbacks
+ clearCallbacks(mCallId);
+ }
+ }
+
+ @Override
+ public void onImageFetchComplete(Bitmap bitmap) {
+ loadImage(null, bitmap, mCallId);
+ onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId);
+ }
+ }
+}