Home | History | Annotate | Download | only in incallui
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.incallui;
     18 
     19 import com.google.common.base.MoreObjects;
     20 import com.google.common.base.Preconditions;
     21 import com.google.common.collect.Maps;
     22 import com.google.common.collect.Sets;
     23 
     24 import android.content.Context;
     25 import android.graphics.Bitmap;
     26 import android.graphics.drawable.BitmapDrawable;
     27 import android.graphics.drawable.Drawable;
     28 import android.location.Address;
     29 import android.media.RingtoneManager;
     30 import android.net.Uri;
     31 import android.os.AsyncTask;
     32 import android.os.Looper;
     33 import android.provider.ContactsContract;
     34 import android.provider.ContactsContract.CommonDataKinds.Phone;
     35 import android.provider.ContactsContract.Contacts;
     36 import android.provider.ContactsContract.DisplayNameSources;
     37 import android.telecom.TelecomManager;
     38 import android.text.TextUtils;
     39 import android.util.Pair;
     40 
     41 import com.android.contacts.common.ContactsUtils;
     42 import com.android.contacts.common.util.PhoneNumberHelper;
     43 import com.android.dialer.R;
     44 import com.android.dialer.calllog.ContactInfo;
     45 import com.android.dialer.service.CachedNumberLookupService;
     46 import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo;
     47 import com.android.dialer.util.MoreStrings;
     48 import com.android.incallui.Call.LogState;
     49 import com.android.incallui.service.PhoneNumberService;
     50 import com.android.incalluibind.ObjectFactory;
     51 
     52 import org.json.JSONException;
     53 import org.json.JSONObject;
     54 
     55 import java.util.Calendar;
     56 import java.util.HashMap;
     57 import java.util.List;
     58 import java.util.Set;
     59 
     60 /**
     61  * Class responsible for querying Contact Information for Call objects. Can perform asynchronous
     62  * requests to the Contact Provider for information as well as respond synchronously for any data
     63  * that it currently has cached from previous queries. This class always gets called from the UI
     64  * thread so it does not need thread protection.
     65  */
     66 public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadCompleteListener {
     67 
     68     private static final String TAG = ContactInfoCache.class.getSimpleName();
     69     private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
     70 
     71     private final Context mContext;
     72     private final PhoneNumberService mPhoneNumberService;
     73     private final CachedNumberLookupService mCachedNumberLookupService;
     74     private final HashMap<String, ContactCacheEntry> mInfoMap = Maps.newHashMap();
     75     private final HashMap<String, Set<ContactInfoCacheCallback>> mCallBacks = Maps.newHashMap();
     76 
     77     private static ContactInfoCache sCache = null;
     78 
     79     private Drawable mDefaultContactPhotoDrawable;
     80     private Drawable mConferencePhotoDrawable;
     81     private ContactUtils mContactUtils;
     82 
     83     public static synchronized ContactInfoCache getInstance(Context mContext) {
     84         if (sCache == null) {
     85             sCache = new ContactInfoCache(mContext.getApplicationContext());
     86         }
     87         return sCache;
     88     }
     89 
     90     private ContactInfoCache(Context context) {
     91         mContext = context;
     92         mPhoneNumberService = ObjectFactory.newPhoneNumberService(context);
     93         mCachedNumberLookupService =
     94                 com.android.dialerbind.ObjectFactory.newCachedNumberLookupService();
     95         mContactUtils = ObjectFactory.getContactUtilsInstance(context);
     96 
     97     }
     98 
     99     public ContactCacheEntry getInfo(String callId) {
    100         return mInfoMap.get(callId);
    101     }
    102 
    103     public static ContactCacheEntry buildCacheEntryFromCall(Context context, Call call,
    104             boolean isIncoming) {
    105         final ContactCacheEntry entry = new ContactCacheEntry();
    106 
    107         // TODO: get rid of caller info.
    108         final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, call);
    109         ContactInfoCache.populateCacheEntry(context, info, entry, call.getNumberPresentation(),
    110                 isIncoming);
    111         return entry;
    112     }
    113 
    114     public void maybeInsertCnapInformationIntoCache(Context context, final Call call,
    115             final CallerInfo info) {
    116         if (mCachedNumberLookupService == null || TextUtils.isEmpty(info.cnapName)
    117                 || mInfoMap.get(call.getId()) != null) {
    118             return;
    119         }
    120         final Context applicationContext = context.getApplicationContext();
    121         Log.i(TAG, "Found contact with CNAP name - inserting into cache");
    122         new AsyncTask<Void, Void, Void>() {
    123             @Override
    124             protected Void doInBackground(Void... params) {
    125                 ContactInfo contactInfo = new ContactInfo();
    126                 CachedContactInfo cacheInfo = mCachedNumberLookupService.buildCachedContactInfo(
    127                         contactInfo);
    128                 cacheInfo.setSource(CachedContactInfo.SOURCE_TYPE_CNAP, "CNAP", 0);
    129                 contactInfo.name = info.cnapName;
    130                 contactInfo.number = call.getNumber();
    131                 contactInfo.type = ContactsContract.CommonDataKinds.Phone.TYPE_MAIN;
    132                 try {
    133                     final JSONObject contactRows = new JSONObject().put(Phone.CONTENT_ITEM_TYPE,
    134                             new JSONObject()
    135                                     .put(Phone.NUMBER, contactInfo.number)
    136                                     .put(Phone.TYPE, Phone.TYPE_MAIN));
    137                     final String jsonString = new JSONObject()
    138                             .put(Contacts.DISPLAY_NAME, contactInfo.name)
    139                             .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME)
    140                             .put(Contacts.CONTENT_ITEM_TYPE, contactRows).toString();
    141                     cacheInfo.setLookupKey(jsonString);
    142                 } catch (JSONException e) {
    143                     Log.w(TAG, "Creation of lookup key failed when caching CNAP information");
    144                 }
    145                 mCachedNumberLookupService.addContact(applicationContext, cacheInfo);
    146                 return null;
    147             }
    148         }.execute();
    149     }
    150 
    151     private class FindInfoCallback implements CallerInfoAsyncQuery.OnQueryCompleteListener {
    152         private final boolean mIsIncoming;
    153 
    154         public FindInfoCallback(boolean isIncoming) {
    155             mIsIncoming = isIncoming;
    156         }
    157 
    158         @Override
    159         public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
    160             findInfoQueryComplete((Call) cookie, callerInfo, mIsIncoming, true);
    161         }
    162     }
    163 
    164     /**
    165      * Requests contact data for the Call object passed in.
    166      * Returns the data through callback.  If callback is null, no response is made, however the
    167      * query is still performed and cached.
    168      *
    169      * @param callback The function to call back when the call is found. Can be null.
    170      */
    171     public void findInfo(final Call call, final boolean isIncoming,
    172             ContactInfoCacheCallback callback) {
    173         Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread());
    174         Preconditions.checkNotNull(callback);
    175 
    176         final String callId = call.getId();
    177         final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
    178         Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
    179 
    180         // If we have a previously obtained intermediate result return that now
    181         if (cacheEntry != null) {
    182             Log.d(TAG, "Contact lookup. In memory cache hit; lookup "
    183                     + (callBacks == null ? "complete" : "still running"));
    184             callback.onContactInfoComplete(callId, cacheEntry);
    185             // If no other callbacks are in flight, we're done.
    186             if (callBacks == null) {
    187                 return;
    188             }
    189         }
    190 
    191         // If the entry already exists, add callback
    192         if (callBacks != null) {
    193             callBacks.add(callback);
    194             return;
    195         }
    196         Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
    197         // New lookup
    198         callBacks = Sets.newHashSet();
    199         callBacks.add(callback);
    200         mCallBacks.put(callId, callBacks);
    201 
    202         /**
    203          * Performs a query for caller information.
    204          * Save any immediate data we get from the query. An asynchronous query may also be made
    205          * for any data that we do not already have. Some queries, such as those for voicemail and
    206          * emergency call information, will not perform an additional asynchronous query.
    207          */
    208         final CallerInfo callerInfo = CallerInfoUtils.getCallerInfoForCall(
    209                 mContext, call, new FindInfoCallback(isIncoming));
    210 
    211         findInfoQueryComplete(call, callerInfo, isIncoming, false);
    212     }
    213 
    214     private void findInfoQueryComplete(Call call, CallerInfo callerInfo, boolean isIncoming,
    215             boolean didLocalLookup) {
    216         final String callId = call.getId();
    217         int presentationMode = call.getNumberPresentation();
    218         if (callerInfo.contactExists || callerInfo.isEmergencyNumber() ||
    219                 callerInfo.isVoiceMailNumber()) {
    220             presentationMode = TelecomManager.PRESENTATION_ALLOWED;
    221         }
    222 
    223         ContactCacheEntry cacheEntry = mInfoMap.get(callId);
    224         // Ensure we always have a cacheEntry. Replace the existing entry if
    225         // it has no name or if we found a local contact.
    226         if (cacheEntry == null || TextUtils.isEmpty(cacheEntry.namePrimary) ||
    227                 callerInfo.contactExists) {
    228             cacheEntry = buildEntry(mContext, callId, callerInfo, presentationMode, isIncoming);
    229             mInfoMap.put(callId, cacheEntry);
    230         }
    231 
    232         sendInfoNotifications(callId, cacheEntry);
    233 
    234         if (didLocalLookup) {
    235             // Before issuing a request for more data from other services, we only check that the
    236             // contact wasn't found in the local DB.  We don't check the if the cache entry already
    237             // has a name because we allow overriding cnap data with data from other services.
    238             if (!callerInfo.contactExists && mPhoneNumberService != null) {
    239                 Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
    240                 final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId);
    241                 mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener,
    242                         isIncoming);
    243             } else if (cacheEntry.displayPhotoUri != null) {
    244                 Log.d(TAG, "Contact lookup. Local contact found, starting image load");
    245                 // Load the image with a callback to update the image state.
    246                 // When the load is finished, onImageLoadComplete() will be called.
    247                 cacheEntry.isLoadingPhoto = true;
    248                 ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
    249                         mContext, cacheEntry.displayPhotoUri, ContactInfoCache.this, callId);
    250             } else {
    251                 if (callerInfo.contactExists) {
    252                     Log.d(TAG, "Contact lookup done. Local contact found, no image.");
    253                 } else {
    254                     Log.d(TAG, "Contact lookup done. Local contact not found and"
    255                             + " no remote lookup service available.");
    256                 }
    257                 clearCallbacks(callId);
    258             }
    259         }
    260     }
    261 
    262     class PhoneNumberServiceListener implements PhoneNumberService.NumberLookupListener,
    263                                      PhoneNumberService.ImageLookupListener, ContactUtils.Listener {
    264         private final String mCallId;
    265 
    266         PhoneNumberServiceListener(String callId) {
    267             mCallId = callId;
    268         }
    269 
    270         @Override
    271         public void onPhoneNumberInfoComplete(
    272                 final PhoneNumberService.PhoneNumberInfo info) {
    273             // If we got a miss, this is the end of the lookup pipeline,
    274             // so clear the callbacks and return.
    275             if (info == null) {
    276                 Log.d(TAG, "Contact lookup done. Remote contact not found.");
    277                 clearCallbacks(mCallId);
    278                 return;
    279             }
    280 
    281             ContactCacheEntry entry = new ContactCacheEntry();
    282             entry.namePrimary = info.getDisplayName();
    283             entry.number = info.getNumber();
    284             entry.contactLookupResult = info.getLookupSource();
    285             final int type = info.getPhoneType();
    286             final String label = info.getPhoneLabel();
    287             if (type == Phone.TYPE_CUSTOM) {
    288                 entry.label = label;
    289             } else {
    290                 final CharSequence typeStr = Phone.getTypeLabel(
    291                         mContext.getResources(), type, label);
    292                 entry.label = typeStr == null ? null : typeStr.toString();
    293             }
    294             final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
    295             if (oldEntry != null) {
    296                 // Location is only obtained from local lookup so persist
    297                 // the value for remote lookups. Once we have a name this
    298                 // field is no longer used; it is persisted here in case
    299                 // the UI is ever changed to use it.
    300                 entry.location = oldEntry.location;
    301                 // Contact specific ringtone is obtained from local lookup.
    302                 entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
    303             }
    304 
    305             // If no image and it's a business, switch to using the default business avatar.
    306             if (info.getImageUrl() == null && info.isBusiness()) {
    307                 Log.d(TAG, "Business has no image. Using default.");
    308                 entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
    309             }
    310 
    311             mInfoMap.put(mCallId, entry);
    312             sendInfoNotifications(mCallId, entry);
    313 
    314             if (mContactUtils != null) {
    315                 // This method will callback "onContactInteractionsFound".
    316                 entry.isLoadingContactInteractions =
    317                         mContactUtils.retrieveContactInteractionsFromLookupKey(
    318                                 info.getLookupKey(), this);
    319             }
    320 
    321             entry.isLoadingPhoto = info.getImageUrl() != null;
    322 
    323             // If there is no image or contact interactions then we should not expect another
    324             // callback.
    325             if (!entry.isLoadingPhoto && !entry.isLoadingContactInteractions) {
    326                 // We're done, so clear callbacks
    327                 clearCallbacks(mCallId);
    328             }
    329         }
    330 
    331         @Override
    332         public void onImageFetchComplete(Bitmap bitmap) {
    333             onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId);
    334         }
    335 
    336         @Override
    337         public void onContactInteractionsFound(Address address,
    338                 List<Pair<Calendar, Calendar>> openingHours) {
    339             final ContactCacheEntry entry = mInfoMap.get(mCallId);
    340             if (entry == null) {
    341                 Log.e(this, "Contact context received for empty search entry.");
    342                 clearCallbacks(mCallId);
    343                 return;
    344             }
    345 
    346             entry.isLoadingContactInteractions = false;
    347 
    348             Log.v(ContactInfoCache.this, "Setting contact interactions for entry: ", entry);
    349 
    350             entry.locationAddress = address;
    351             entry.openingHours = openingHours;
    352             sendContactInteractionsNotifications(mCallId, entry);
    353 
    354             if (!entry.isLoadingPhoto) {
    355                 clearCallbacks(mCallId);
    356             }
    357         }
    358     }
    359 
    360     /**
    361      * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface.
    362      * make sure that the call state is reflected after the image is loaded.
    363      */
    364     @Override
    365     public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
    366         Log.d(this, "Image load complete with context: ", mContext);
    367         // TODO: may be nice to update the image view again once the newer one
    368         // is available on contacts database.
    369 
    370         final String callId = (String) cookie;
    371         final ContactCacheEntry entry = mInfoMap.get(callId);
    372 
    373         if (entry == null) {
    374             Log.e(this, "Image Load received for empty search entry.");
    375             clearCallbacks(callId);
    376             return;
    377         }
    378 
    379         entry.isLoadingPhoto = false;
    380 
    381         Log.d(this, "setting photo for entry: ", entry);
    382 
    383         // Conference call icons are being handled in CallCardPresenter.
    384         if (photo != null) {
    385             Log.v(this, "direct drawable: ", photo);
    386             entry.photo = photo;
    387         } else if (photoIcon != null) {
    388             Log.v(this, "photo icon: ", photoIcon);
    389             entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
    390         } else {
    391             Log.v(this, "unknown photo");
    392             entry.photo = null;
    393         }
    394 
    395         sendImageNotifications(callId, entry);
    396 
    397         if (!entry.isLoadingContactInteractions) {
    398             clearCallbacks(callId);
    399         }
    400     }
    401 
    402     /**
    403      * Blows away the stored cache values.
    404      */
    405     public void clearCache() {
    406         mInfoMap.clear();
    407         mCallBacks.clear();
    408     }
    409 
    410     private ContactCacheEntry buildEntry(Context context, String callId,
    411             CallerInfo info, int presentation, boolean isIncoming) {
    412         // The actual strings we're going to display onscreen:
    413         Drawable photo = null;
    414 
    415         final ContactCacheEntry cce = new ContactCacheEntry();
    416         populateCacheEntry(context, info, cce, presentation, isIncoming);
    417 
    418         // This will only be true for emergency numbers
    419         if (info.photoResource != 0) {
    420             photo = context.getResources().getDrawable(info.photoResource);
    421         } else if (info.isCachedPhotoCurrent) {
    422             if (info.cachedPhoto != null) {
    423                 photo = info.cachedPhoto;
    424             } else {
    425                 photo = getDefaultContactPhotoDrawable();
    426             }
    427         } else if (info.contactDisplayPhotoUri == null) {
    428             photo = getDefaultContactPhotoDrawable();
    429         } else {
    430             cce.displayPhotoUri = info.contactDisplayPhotoUri;
    431         }
    432 
    433         // Support any contact id in N because QuickContacts in N starts supporting enterprise
    434         // contact id
    435         if (info.lookupKeyOrNull != null
    436                 && (ContactsUtils.FLAG_N_FEATURE || info.contactIdOrZero != 0)) {
    437             cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull);
    438         } else {
    439             Log.v(TAG, "lookup key is null or contact ID is 0 on M. Don't create a lookup uri.");
    440             cce.lookupUri = null;
    441         }
    442 
    443         cce.photo = photo;
    444         cce.lookupKey = info.lookupKeyOrNull;
    445         cce.contactRingtoneUri = info.contactRingtoneUri;
    446         if (cce.contactRingtoneUri == null || cce.contactRingtoneUri == Uri.EMPTY) {
    447             cce.contactRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
    448         }
    449 
    450         return cce;
    451     }
    452 
    453     /**
    454      * Populate a cache entry from a call (which got converted into a caller info).
    455      */
    456     public static void populateCacheEntry(Context context, CallerInfo info, ContactCacheEntry cce,
    457             int presentation, boolean isIncoming) {
    458         Preconditions.checkNotNull(info);
    459         String displayName = null;
    460         String displayNumber = null;
    461         String displayLocation = null;
    462         String label = null;
    463         boolean isSipCall = false;
    464 
    465             // It appears that there is a small change in behaviour with the
    466             // PhoneUtils' startGetCallerInfo whereby if we query with an
    467             // empty number, we will get a valid CallerInfo object, but with
    468             // fields that are all null, and the isTemporary boolean input
    469             // parameter as true.
    470 
    471             // In the past, we would see a NULL callerinfo object, but this
    472             // ends up causing null pointer exceptions elsewhere down the
    473             // line in other cases, so we need to make this fix instead. It
    474             // appears that this was the ONLY call to PhoneUtils
    475             // .getCallerInfo() that relied on a NULL CallerInfo to indicate
    476             // an unknown contact.
    477 
    478             // Currently, infi.phoneNumber may actually be a SIP address, and
    479             // if so, it might sometimes include the "sip:" prefix. That
    480             // prefix isn't really useful to the user, though, so strip it off
    481             // if present. (For any other URI scheme, though, leave the
    482             // prefix alone.)
    483             // TODO: It would be cleaner for CallerInfo to explicitly support
    484             // SIP addresses instead of overloading the "phoneNumber" field.
    485             // Then we could remove this hack, and instead ask the CallerInfo
    486             // for a "user visible" form of the SIP address.
    487             String number = info.phoneNumber;
    488 
    489             if (!TextUtils.isEmpty(number)) {
    490                 isSipCall = PhoneNumberHelper.isUriNumber(number);
    491                 if (number.startsWith("sip:")) {
    492                     number = number.substring(4);
    493                 }
    494             }
    495 
    496             if (TextUtils.isEmpty(info.name)) {
    497                 // No valid "name" in the CallerInfo, so fall back to
    498                 // something else.
    499                 // (Typically, we promote the phone number up to the "name" slot
    500                 // onscreen, and possibly display a descriptive string in the
    501                 // "number" slot.)
    502                 if (TextUtils.isEmpty(number)) {
    503                     // No name *or* number! Display a generic "unknown" string
    504                     // (or potentially some other default based on the presentation.)
    505                     displayName = getPresentationString(context, presentation, info.callSubject);
    506                     Log.d(TAG, "  ==> no name *or* number! displayName = " + displayName);
    507                 } else if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
    508                     // This case should never happen since the network should never send a phone #
    509                     // AND a restricted presentation. However we leave it here in case of weird
    510                     // network behavior
    511                     displayName = getPresentationString(context, presentation, info.callSubject);
    512                     Log.d(TAG, "  ==> presentation not allowed! displayName = " + displayName);
    513                 } else if (!TextUtils.isEmpty(info.cnapName)) {
    514                     // No name, but we do have a valid CNAP name, so use that.
    515                     displayName = info.cnapName;
    516                     info.name = info.cnapName;
    517                     displayNumber = number;
    518                     Log.d(TAG, "  ==> cnapName available: displayName '" + displayName +
    519                             "', displayNumber '" + displayNumber + "'");
    520                 } else {
    521                     // No name; all we have is a number. This is the typical
    522                     // case when an incoming call doesn't match any contact,
    523                     // or if you manually dial an outgoing number using the
    524                     // dialpad.
    525                     displayNumber = number;
    526 
    527                     // Display a geographical description string if available
    528                     // (but only for incoming calls.)
    529                     if (isIncoming) {
    530                         // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
    531                         // query to only do the geoDescription lookup in the first
    532                         // place for incoming calls.
    533                         displayLocation = info.geoDescription; // may be null
    534                         Log.d(TAG, "Geodescrption: " + info.geoDescription);
    535                     }
    536 
    537                     Log.d(TAG, "  ==>  no name; falling back to number:"
    538                             + " displayNumber '" + Log.pii(displayNumber)
    539                             + "', displayLocation '" + displayLocation + "'");
    540                 }
    541             } else {
    542                 // We do have a valid "name" in the CallerInfo. Display that
    543                 // in the "name" slot, and the phone number in the "number" slot.
    544                 if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
    545                     // This case should never happen since the network should never send a name
    546                     // AND a restricted presentation. However we leave it here in case of weird
    547                     // network behavior
    548                     displayName = getPresentationString(context, presentation, info.callSubject);
    549                     Log.d(TAG, "  ==> valid name, but presentation not allowed!" +
    550                             " displayName = " + displayName);
    551                 } else {
    552                     // Causes cce.namePrimary to be set as info.name below. CallCardPresenter will
    553                     // later determine whether to use the name or nameAlternative when presenting
    554                     displayName = info.name;
    555                     cce.nameAlternative = info.nameAlternative;
    556                     displayNumber = number;
    557                     label = info.phoneLabel;
    558                     Log.d(TAG, "  ==>  name is present in CallerInfo: displayName '" + displayName
    559                             + "', displayNumber '" + displayNumber + "'");
    560                 }
    561             }
    562 
    563         cce.namePrimary = displayName;
    564         cce.number = displayNumber;
    565         cce.location = displayLocation;
    566         cce.label = label;
    567         cce.isSipCall = isSipCall;
    568         cce.userType = info.userType;
    569 
    570         if (info.contactExists) {
    571             cce.contactLookupResult = LogState.LOOKUP_LOCAL_CONTACT;
    572         }
    573     }
    574 
    575     /**
    576      * Sends the updated information to call the callbacks for the entry.
    577      */
    578     private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
    579         final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
    580         if (callBacks != null) {
    581             for (ContactInfoCacheCallback callBack : callBacks) {
    582                 callBack.onContactInfoComplete(callId, entry);
    583             }
    584         }
    585     }
    586 
    587     private void sendImageNotifications(String callId, ContactCacheEntry entry) {
    588         final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
    589         if (callBacks != null && entry.photo != null) {
    590             for (ContactInfoCacheCallback callBack : callBacks) {
    591                 callBack.onImageLoadComplete(callId, entry);
    592             }
    593         }
    594     }
    595 
    596     private void sendContactInteractionsNotifications(String callId, ContactCacheEntry entry) {
    597         final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
    598         if (callBacks != null) {
    599             for (ContactInfoCacheCallback callBack : callBacks) {
    600                 callBack.onContactInteractionsInfoComplete(callId, entry);
    601             }
    602         }
    603     }
    604 
    605     private void clearCallbacks(String callId) {
    606         mCallBacks.remove(callId);
    607     }
    608 
    609     /**
    610      * Gets name strings based on some special presentation modes and the associated custom label.
    611      */
    612     private static String getPresentationString(Context context, int presentation,
    613              String customLabel) {
    614         String name = context.getString(R.string.unknown);
    615         if (!TextUtils.isEmpty(customLabel) &&
    616                 ((presentation == TelecomManager.PRESENTATION_UNKNOWN) ||
    617                  (presentation == TelecomManager.PRESENTATION_RESTRICTED))) {
    618             name = customLabel;
    619             return name;
    620         } else {
    621             if (presentation == TelecomManager.PRESENTATION_RESTRICTED) {
    622                 name = context.getString(R.string.private_num);
    623             } else if (presentation == TelecomManager.PRESENTATION_PAYPHONE) {
    624                 name = context.getString(R.string.payphone);
    625             }
    626         }
    627         return name;
    628     }
    629 
    630     public Drawable getDefaultContactPhotoDrawable() {
    631         if (mDefaultContactPhotoDrawable == null) {
    632             mDefaultContactPhotoDrawable =
    633                     mContext.getResources().getDrawable(R.drawable.img_no_image_automirrored);
    634         }
    635         return mDefaultContactPhotoDrawable;
    636     }
    637 
    638     public Drawable getConferenceDrawable() {
    639         if (mConferencePhotoDrawable == null) {
    640             mConferencePhotoDrawable =
    641                     mContext.getResources().getDrawable(R.drawable.img_conference_automirrored);
    642         }
    643         return mConferencePhotoDrawable;
    644     }
    645 
    646     /**
    647      * Callback interface for the contact query.
    648      */
    649     public interface ContactInfoCacheCallback {
    650         public void onContactInfoComplete(String callId, ContactCacheEntry entry);
    651         public void onImageLoadComplete(String callId, ContactCacheEntry entry);
    652         public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry);
    653     }
    654 
    655     public static class ContactCacheEntry {
    656         public String namePrimary;
    657         public String nameAlternative;
    658         public String number;
    659         public String location;
    660         public String label;
    661         public Drawable photo;
    662         public boolean isSipCall;
    663         // Note in cache entry whether this is a pending async loading action to know whether to
    664         // wait for its callback or not.
    665         public boolean isLoadingPhoto;
    666         public boolean isLoadingContactInteractions;
    667         /** This will be used for the "view" notification. */
    668         public Uri contactUri;
    669         /** Either a display photo or a thumbnail URI. */
    670         public Uri displayPhotoUri;
    671         public Uri lookupUri; // Sent to NotificationMananger
    672         public String lookupKey;
    673         public Address locationAddress;
    674         public List<Pair<Calendar, Calendar>> openingHours;
    675         public int contactLookupResult = LogState.LOOKUP_NOT_FOUND;
    676         public long userType = ContactsUtils.USER_TYPE_CURRENT;
    677         public Uri contactRingtoneUri;
    678 
    679         @Override
    680         public String toString() {
    681             return MoreObjects.toStringHelper(this)
    682                     .add("name", MoreStrings.toSafeString(namePrimary))
    683                     .add("nameAlternative", MoreStrings.toSafeString(nameAlternative))
    684                     .add("number", MoreStrings.toSafeString(number))
    685                     .add("location", MoreStrings.toSafeString(location))
    686                     .add("label", label)
    687                     .add("photo", photo)
    688                     .add("isSipCall", isSipCall)
    689                     .add("contactUri", contactUri)
    690                     .add("displayPhotoUri", displayPhotoUri)
    691                     .add("locationAddress", locationAddress)
    692                     .add("openingHours", openingHours)
    693                     .add("contactLookupResult", contactLookupResult)
    694                     .add("userType", userType)
    695                     .add("contactRingtoneUri", contactRingtoneUri)
    696                     .toString();
    697         }
    698     }
    699 }
    700