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