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.java952
1 files changed, 952 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..e45eb9746
--- /dev/null
+++ b/java/com/android/incallui/ContactInfoCache.java
@@ -0,0 +1,952 @@
+/*
+ * 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.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.SystemClock;
+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.Nullable;
+import android.support.annotation.WorkerThread;
+import android.support.v4.os.UserManagerCompat;
+import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
+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.common.concurrent.DialerExecutor;
+import com.android.dialer.common.concurrent.DialerExecutor.Worker;
+import com.android.dialer.common.concurrent.DialerExecutors;
+import com.android.dialer.logging.ContactLookupResult;
+import com.android.dialer.logging.ContactSource;
+import com.android.dialer.oem.CequintCallerIdManager;
+import com.android.dialer.oem.CequintCallerIdManager.CequintCallerIdContact;
+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 ConcurrentHashMap<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
+ private final Map<String, Set<ContactInfoCacheCallback>> mCallBacks = new ArrayMap<>();
+ private Drawable mDefaultContactPhotoDrawable;
+ private int mQueryId;
+ private final DialerExecutor<CnapInformationWrapper> cachedNumberLookupExecutor =
+ DialerExecutors.createNonUiTaskBuilder(new CachedNumberLookupWorker()).build();
+
+ private static class CachedNumberLookupWorker implements Worker<CnapInformationWrapper, Void> {
+ @Nullable
+ @Override
+ public Void doInBackground(@Nullable CnapInformationWrapper input) {
+ if (input == null) {
+ return null;
+ }
+ ContactInfo contactInfo = new ContactInfo();
+ CachedContactInfo cacheInfo = input.service.buildCachedContactInfo(contactInfo);
+ cacheInfo.setSource(ContactSource.Type.SOURCE_TYPE_CNAP, "CNAP", 0);
+ contactInfo.name = input.cnapName;
+ contactInfo.number = input.number;
+ try {
+ final JSONObject contactRows =
+ new JSONObject()
+ .put(
+ Phone.CONTENT_ITEM_TYPE,
+ new JSONObject().put(Phone.NUMBER, contactInfo.number));
+ 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");
+ }
+ input.service.addContact(input.context.getApplicationContext(), cacheInfo);
+ return null;
+ }
+ }
+
+ 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;
+ }
+
+ 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());
+ return entry;
+ }
+
+ /** Populate a cache entry from a call (which got converted into a caller info). */
+ private static void populateCacheEntry(
+ @NonNull Context context,
+ @NonNull CallerInfo info,
+ @NonNull ContactCacheEntry cce,
+ int presentation) {
+ Objects.requireNonNull(info);
+ String displayName = null;
+ String displayNumber = 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) && TextUtils.isEmpty(info.cnapName)) {
+ // 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);
+
+ Log.d(
+ TAG,
+ " ==> no name; falling back to number:"
+ + " displayNumber '"
+ + Log.pii(displayNumber)
+ + "'");
+ }
+ } 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 = info.geoDescription;
+ cce.label = label;
+ cce.isSipCall = isSipCall;
+ cce.userType = info.userType;
+ cce.originalPhoneNumber = info.phoneNumber;
+ cce.shouldShowLocation = info.shouldShowGeoDescription;
+
+ 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;
+ }
+
+ ContactCacheEntry getInfo(String callId) {
+ return mInfoMap.get(callId);
+ }
+
+ private static final class CnapInformationWrapper {
+ final String number;
+ final String cnapName;
+ final Context context;
+ final CachedNumberLookupService service;
+
+ CnapInformationWrapper(
+ String number, String cnapName, Context context, CachedNumberLookupService service) {
+ this.number = number;
+ this.cnapName = cnapName;
+ this.context = context;
+ this.service = service;
+ }
+ }
+
+ 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;
+ }
+ Log.i(TAG, "Found contact with CNAP name - inserting into cache");
+
+ cachedNumberLookupExecutor.executeParallel(
+ new CnapInformationWrapper(
+ call.getNumber(), info.cnapName, context, cachedNumberLookupService));
+ }
+
+ /**
+ * 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);
+
+ // We need to force a new query if phone number has changed.
+ boolean forceQuery = needForceQuery(call, cacheEntry);
+ Log.d(TAG, "findInfo: callId = " + callId + "; forceQuery = " + forceQuery);
+
+ // If we have a previously obtained intermediate result return that now except needs
+ // force query.
+ if (cacheEntry != null && !forceQuery) {
+ 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) {
+ Log.d(TAG, "Another query is in progress, add callback only.");
+ callBacks.add(callback);
+ if (!forceQuery) {
+ Log.d(TAG, "No need to query again, just return and wait for existing query to finish");
+ return;
+ }
+ } else {
+ 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 CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryId, callId);
+ mQueryId++;
+ final CallerInfo callerInfo =
+ CallerInfoUtils.getCallerInfoForCall(
+ mContext,
+ call,
+ new DialerCallCookieWrapper(callId, call.getNumberPresentation(), call.getCnapName()),
+ new FindInfoCallback(isIncoming, queryToken));
+
+ if (cacheEntry != null) {
+ // We should not override the old cache item until the new query is
+ // back. We should only update the queryId. Otherwise, we may see
+ // flicker of the name and image (old cache -> new cache before query
+ // -> new cache after query)
+ cacheEntry.queryId = queryToken.mQueryId;
+ Log.d(TAG, "There is an existing cache. Do not override until new query is back");
+ } else {
+ ContactCacheEntry initialCacheEntry =
+ updateCallerInfoInCacheOnAnyThread(
+ callId, call.getNumberPresentation(), callerInfo, isIncoming, false, queryToken);
+ sendInfoNotifications(callId, initialCacheEntry);
+ }
+ }
+
+ @AnyThread
+ private ContactCacheEntry updateCallerInfoInCacheOnAnyThread(
+ String callId,
+ int numberPresentation,
+ CallerInfo callerInfo,
+ boolean isIncoming,
+ boolean didLocalLookup,
+ CallerInfoQueryToken queryToken) {
+ Log.d(
+ TAG,
+ "updateCallerInfoInCacheOnAnyThread: callId = "
+ + callId
+ + "; queryId = "
+ + queryToken.mQueryId
+ + "; didLocalLookup = "
+ + didLocalLookup);
+
+ int presentationMode = numberPresentation;
+ if (callerInfo.contactExists
+ || callerInfo.isEmergencyNumber()
+ || callerInfo.isVoiceMailNumber()) {
+ presentationMode = TelecomManager.PRESENTATION_ALLOWED;
+ }
+
+ // We always replace the entry. The only exception is the same photo case.
+ ContactCacheEntry cacheEntry = buildEntry(mContext, callerInfo, presentationMode);
+ cacheEntry.queryId = queryToken.mQueryId;
+
+ ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
+ Log.d(TAG, "Existing cacheEntry in hashMap " + existingCacheEntry);
+
+ 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, queryToken.mQueryId);
+ cacheEntry.hasPendingQuery = true;
+ mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
+ } else if (cacheEntry.displayPhotoUri != null) {
+ // When the difference between 2 numbers is only the prefix (e.g. + or IDD),
+ // we will still trigger force query so that the number can be updated on
+ // the calling screen. We need not query the image again if the previous
+ // query already has the image to avoid flickering.
+ if (existingCacheEntry != null
+ && existingCacheEntry.displayPhotoUri != null
+ && existingCacheEntry.displayPhotoUri.equals(cacheEntry.displayPhotoUri)
+ && existingCacheEntry.photo != null) {
+ Log.d(TAG, "Same picture. Do not need start image load.");
+ cacheEntry.photo = existingCacheEntry.photo;
+ cacheEntry.photoType = existingCacheEntry.photoType;
+ return cacheEntry;
+ }
+
+ 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.hasPendingQuery = true;
+ ContactsAsyncHelper.startObtainPhotoAsync(
+ TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
+ mContext,
+ cacheEntry.displayPhotoUri,
+ ContactInfoCache.this,
+ queryToken);
+ }
+ Log.d(TAG, "put entry into map: " + cacheEntry);
+ mInfoMap.put(callId, cacheEntry);
+ } else {
+ // Don't overwrite if there is existing cache.
+ Log.d(TAG, "put entry into map if not exists: " + cacheEntry);
+ mInfoMap.putIfAbsent(callId, cacheEntry);
+ }
+ return cacheEntry;
+ }
+
+ private void maybeUpdateFromCequintCallerId(
+ CallerInfo callerInfo, String cnapName, boolean isIncoming) {
+ if (!CequintCallerIdManager.isCequintCallerIdEnabled(mContext)) {
+ return;
+ }
+ if (callerInfo.phoneNumber == null) {
+ return;
+ }
+ CequintCallerIdContact cequintCallerIdContact =
+ CequintCallerIdManager.getCequintCallerIdContactForInCall(
+ mContext, callerInfo.phoneNumber, cnapName, isIncoming);
+
+ if (cequintCallerIdContact == null) {
+ return;
+ }
+ boolean hasUpdate = false;
+
+ if (TextUtils.isEmpty(callerInfo.name) && !TextUtils.isEmpty(cequintCallerIdContact.name)) {
+ callerInfo.name = cequintCallerIdContact.name;
+ hasUpdate = true;
+ }
+ if (!TextUtils.isEmpty(cequintCallerIdContact.geoDescription)) {
+ callerInfo.geoDescription = cequintCallerIdContact.geoDescription;
+ callerInfo.shouldShowGeoDescription = true;
+ hasUpdate = true;
+ }
+ // Don't overwrite photo in local contacts.
+ if (!callerInfo.contactExists
+ && callerInfo.contactDisplayPhotoUri == null
+ && cequintCallerIdContact.imageUrl != null) {
+ callerInfo.contactDisplayPhotoUri = Uri.parse(cequintCallerIdContact.imageUrl);
+ hasUpdate = true;
+ }
+ // Set contact to exist to avoid phone number service lookup.
+ callerInfo.contactExists = hasUpdate;
+ }
+
+ /**
+ * 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();
+ CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+ final String callId = myCookie.mCallId;
+ final int queryId = myCookie.mQueryId;
+ if (!isWaitingForThisQuery(callId, queryId)) {
+ return;
+ }
+ loadImage(photo, photoIcon, cookie);
+ }
+
+ private void loadImage(Drawable photo, Bitmap photoIcon, Object cookie) {
+ Log.d(TAG, "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.
+ CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+ final String callId = myCookie.mCallId;
+ ContactCacheEntry entry = mInfoMap.get(callId);
+
+ if (entry == null) {
+ Log.e(TAG, "Image Load received for empty search entry.");
+ clearCallbacks(callId);
+ return;
+ }
+
+ Log.d(TAG, "setting photo for entry: ", entry);
+
+ // Conference call icons are being handled in CallCardPresenter.
+ if (photo != null) {
+ Log.v(TAG, "direct drawable: ", photo);
+ entry.photo = photo;
+ entry.photoType = ContactPhotoType.CONTACT;
+ } else if (photoIcon != null) {
+ Log.v(TAG, "photo icon: ", photoIcon);
+ entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
+ entry.photoType = ContactPhotoType.CONTACT;
+ } else {
+ Log.v(TAG, "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();
+ CallerInfoQueryToken myCookie = (CallerInfoQueryToken) cookie;
+ final String callId = myCookie.mCallId;
+ final int queryId = myCookie.mQueryId;
+ if (!isWaitingForThisQuery(callId, queryId)) {
+ return;
+ }
+ sendImageNotifications(callId, mInfoMap.get(callId));
+
+ clearCallbacks(callId);
+ }
+
+ /** Blows away the stored cache values. */
+ public void clearCache() {
+ mInfoMap.clear();
+ mCallBacks.clear();
+ mQueryId = 0;
+ }
+
+ private ContactCacheEntry buildEntry(Context context, CallerInfo info, int presentation) {
+ final ContactCacheEntry cce = new ContactCacheEntry();
+ populateCacheEntry(context, info, cce, presentation);
+
+ // 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 {
+ 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. */
+ @MainThread
+ private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
+ Assert.isMainThread();
+ final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+ if (callBacks != null) {
+ for (ContactInfoCacheCallback callBack : callBacks) {
+ callBack.onContactInfoComplete(callId, entry);
+ }
+ }
+ }
+
+ @MainThread
+ private void sendImageNotifications(String callId, ContactCacheEntry entry) {
+ Assert.isMainThread();
+ 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;
+ }
+
+ /** 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 int photoType;
+ boolean isSipCall;
+ // Note in cache entry whether this is a pending async loading action to know whether to
+ // wait for its callback or not.
+ boolean hasPendingQuery;
+ /** This will be used for the "view" notification. */
+ public Uri contactUri;
+ /** Either a display photo or a thumbnail URI. */
+ Uri displayPhotoUri;
+
+ public Uri lookupUri; // Sent to NotificationMananger
+ public String lookupKey;
+ public ContactLookupResult.Type contactLookupResult = ContactLookupResult.Type.NOT_FOUND;
+ public long userType = ContactsUtils.USER_TYPE_CURRENT;
+ Uri contactRingtoneUri;
+ /** Query id to identify the query session. */
+ int queryId;
+ /** The phone number without any changes to display to the user (ex: cnap...) */
+ String originalPhoneNumber;
+ boolean shouldShowLocation;
+
+ boolean isBusiness;
+
+ @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
+ + ", queryId="
+ + queryId
+ + ", originalPhoneNumber="
+ + originalPhoneNumber
+ + ", shouldShowLocation="
+ + shouldShowLocation
+ + '}';
+ }
+ }
+
+ private static final class DialerCallCookieWrapper {
+ final String callId;
+ final int numberPresentation;
+ final String cnapName;
+
+ DialerCallCookieWrapper(String callId, int numberPresentation, String cnapName) {
+ this.callId = callId;
+ this.numberPresentation = numberPresentation;
+ this.cnapName = cnapName;
+ }
+ }
+
+ private class FindInfoCallback implements OnQueryCompleteListener {
+
+ private final boolean mIsIncoming;
+ private final CallerInfoQueryToken mQueryToken;
+
+ FindInfoCallback(boolean isIncoming, CallerInfoQueryToken queryToken) {
+ mIsIncoming = isIncoming;
+ mQueryToken = queryToken;
+ }
+
+ @Override
+ public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ Assert.isWorkerThread();
+ DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
+ if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
+ return;
+ }
+ long start = SystemClock.uptimeMillis();
+ maybeUpdateFromCequintCallerId(ci, cw.cnapName, mIsIncoming);
+ long time = SystemClock.uptimeMillis() - start;
+ Log.d(TAG, "Cequint Caller Id look up takes " + time + " ms.");
+ updateCallerInfoInCacheOnAnyThread(cw.callId, cw.numberPresentation, ci, mIsIncoming, true, mQueryToken);
+ }
+
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
+ Assert.isMainThread();
+ DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
+ String callId = cw.callId;
+ if (!isWaitingForThisQuery(cw.callId, mQueryToken.mQueryId)) {
+ return;
+ }
+ 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.hasPendingQuery) {
+ 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;
+ private final int mQueryIdOfRemoteLookup;
+
+ PhoneNumberServiceListener(String callId, int queryId) {
+ mCallId = callId;
+ mQueryIdOfRemoteLookup = queryId;
+ }
+
+ @Override
+ public void onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info) {
+ Log.d(TAG, "PhoneNumberServiceListener.onPhoneNumberInfoComplete");
+ if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
+ return;
+ }
+
+ // 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();
+ entry.isBusiness = info.isBusiness();
+ 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();
+ }
+ 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;
+ entry.shouldShowLocation = oldEntry.shouldShowLocation;
+ // 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;
+ }
+
+ Log.d(TAG, "put entry into map: " + entry);
+ mInfoMap.put(mCallId, entry);
+ sendInfoNotifications(mCallId, entry);
+
+ entry.hasPendingQuery = info.getImageUrl() != null;
+
+ // If there is no image then we should not expect another callback.
+ if (!entry.hasPendingQuery) {
+ // We're done, so clear callbacks
+ clearCallbacks(mCallId);
+ }
+ }
+
+ @Override
+ public void onImageFetchComplete(Bitmap bitmap) {
+ Log.d(TAG, "PhoneNumberServiceListener.onImageFetchComplete");
+ if (!isWaitingForThisQuery(mCallId, mQueryIdOfRemoteLookup)) {
+ return;
+ }
+ CallerInfoQueryToken queryToken = new CallerInfoQueryToken(mQueryIdOfRemoteLookup, mCallId);
+ loadImage(null, bitmap, queryToken);
+ onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, queryToken);
+ }
+ }
+
+ private boolean needForceQuery(DialerCall call, ContactCacheEntry cacheEntry) {
+ if (call == null || call.isConferenceCall()) {
+ return false;
+ }
+
+ String newPhoneNumber = PhoneNumberUtils.stripSeparators(call.getNumber());
+ if (cacheEntry == null) {
+ // No info in the map yet so it is the 1st query
+ Log.d(TAG, "needForceQuery: first query");
+ return true;
+ }
+ String oldPhoneNumber = PhoneNumberUtils.stripSeparators(cacheEntry.originalPhoneNumber);
+
+ if (!TextUtils.equals(oldPhoneNumber, newPhoneNumber)) {
+ Log.d(TAG, "phone number has changed: " + oldPhoneNumber + " -> " + newPhoneNumber);
+ return true;
+ }
+
+ return false;
+ }
+
+ private static final class CallerInfoQueryToken {
+ final int mQueryId;
+ final String mCallId;
+
+ CallerInfoQueryToken(int queryId, String callId) {
+ mQueryId = queryId;
+ mCallId = callId;
+ }
+ }
+
+ /** Check if the queryId in the cached map is the same as the one from query result. */
+ private boolean isWaitingForThisQuery(String callId, int queryId) {
+ final ContactCacheEntry existingCacheEntry = mInfoMap.get(callId);
+ if (existingCacheEntry == null) {
+ // This might happen if lookup on background thread comes back before the initial entry is
+ // created.
+ Log.d(TAG, "Cached entry is null.");
+ return true;
+ } else {
+ int waitingQueryId = existingCacheEntry.queryId;
+ Log.d(TAG, "waitingQueryId = " + waitingQueryId + "; queryId = " + queryId);
+ return waitingQueryId == queryId;
+ }
+ }
+}