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 
getInstance(Context mContext)83     public static synchronized ContactInfoCache getInstance(Context mContext) {
84         if (sCache == null) {
85             sCache = new ContactInfoCache(mContext.getApplicationContext());
86         }
87         return sCache;
88     }
89 
ContactInfoCache(Context context)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 
getInfo(String callId)99     public ContactCacheEntry getInfo(String callId) {
100         return mInfoMap.get(callId);
101     }
102 
buildCacheEntryFromCall(Context context, Call call, boolean isIncoming)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 
maybeInsertCnapInformationIntoCache(Context context, final Call call, final CallerInfo info)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 
FindInfoCallback(boolean isIncoming)154         public FindInfoCallback(boolean isIncoming) {
155             mIsIncoming = isIncoming;
156         }
157 
158         @Override
onQueryComplete(int token, Object cookie, CallerInfo callerInfo)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      */
findInfo(final Call call, final boolean isIncoming, ContactInfoCacheCallback callback)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 
findInfoQueryComplete(Call call, CallerInfo callerInfo, boolean isIncoming, boolean didLocalLookup)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 
PhoneNumberServiceListener(String callId)266         PhoneNumberServiceListener(String callId) {
267             mCallId = callId;
268         }
269 
270         @Override
onPhoneNumberInfoComplete( final PhoneNumberService.PhoneNumberInfo info)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
onImageFetchComplete(Bitmap bitmap)332         public void onImageFetchComplete(Bitmap bitmap) {
333             onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId);
334         }
335 
336         @Override
onContactInteractionsFound(Address address, List<Pair<Calendar, Calendar>> openingHours)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
onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie)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      */
clearCache()405     public void clearCache() {
406         mInfoMap.clear();
407         mCallBacks.clear();
408     }
409 
buildEntry(Context context, String callId, CallerInfo info, int presentation, boolean isIncoming)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      */
populateCacheEntry(Context context, CallerInfo info, ContactCacheEntry cce, int presentation, boolean isIncoming)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      */
sendInfoNotifications(String callId, ContactCacheEntry entry)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 
sendImageNotifications(String callId, ContactCacheEntry entry)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 
sendContactInteractionsNotifications(String callId, ContactCacheEntry entry)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 
clearCallbacks(String callId)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      */
getPresentationString(Context context, int presentation, String customLabel)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 
getDefaultContactPhotoDrawable()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 
getConferenceDrawable()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 {
onContactInfoComplete(String callId, ContactCacheEntry entry)650         public void onContactInfoComplete(String callId, ContactCacheEntry entry);
onImageLoadComplete(String callId, ContactCacheEntry entry)651         public void onImageLoadComplete(String callId, ContactCacheEntry entry);
onContactInteractionsInfoComplete(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
toString()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