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