1 /* 2 * Copyright (C) 2011 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.contacts.calllog; 18 19 import com.android.common.widget.GroupingListAdapter; 20 import com.android.contacts.ContactPhotoManager; 21 import com.android.contacts.PhoneCallDetails; 22 import com.android.contacts.PhoneCallDetailsHelper; 23 import com.android.contacts.R; 24 import com.android.contacts.util.ExpirableCache; 25 import com.android.contacts.util.UriUtils; 26 import com.google.common.annotations.VisibleForTesting; 27 28 import android.content.ContentValues; 29 import android.content.Context; 30 import android.content.res.Resources; 31 import android.database.Cursor; 32 import android.net.Uri; 33 import android.os.Handler; 34 import android.os.Message; 35 import android.provider.CallLog.Calls; 36 import android.provider.ContactsContract.PhoneLookup; 37 import android.text.TextUtils; 38 import android.view.LayoutInflater; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.ViewTreeObserver; 42 43 import java.util.LinkedList; 44 45 import libcore.util.Objects; 46 47 /** 48 * Adapter class to fill in data for the Call Log. 49 */ 50 /*package*/ class CallLogAdapter extends GroupingListAdapter 51 implements Runnable, ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator { 52 /** Interface used to initiate a refresh of the content. */ 53 public interface CallFetcher { 54 public void fetchCalls(); 55 } 56 57 /** 58 * Stores a phone number of a call with the country code where it originally occurred. 59 * <p> 60 * Note the country does not necessarily specifies the country of the phone number itself, but 61 * it is the country in which the user was in when the call was placed or received. 62 */ 63 private static final class NumberWithCountryIso { 64 public final String number; 65 public final String countryIso; 66 67 public NumberWithCountryIso(String number, String countryIso) { 68 this.number = number; 69 this.countryIso = countryIso; 70 } 71 72 @Override 73 public boolean equals(Object o) { 74 if (o == null) return false; 75 if (!(o instanceof NumberWithCountryIso)) return false; 76 NumberWithCountryIso other = (NumberWithCountryIso) o; 77 return TextUtils.equals(number, other.number) 78 && TextUtils.equals(countryIso, other.countryIso); 79 } 80 81 @Override 82 public int hashCode() { 83 return (number == null ? 0 : number.hashCode()) 84 ^ (countryIso == null ? 0 : countryIso.hashCode()); 85 } 86 } 87 88 /** The time in millis to delay starting the thread processing requests. */ 89 private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000; 90 91 /** The size of the cache of contact info. */ 92 private static final int CONTACT_INFO_CACHE_SIZE = 100; 93 94 private final Context mContext; 95 private final ContactInfoHelper mContactInfoHelper; 96 private final CallFetcher mCallFetcher; 97 98 /** 99 * A cache of the contact details for the phone numbers in the call log. 100 * <p> 101 * The content of the cache is expired (but not purged) whenever the application comes to 102 * the foreground. 103 * <p> 104 * The key is number with the country in which the call was placed or received. 105 */ 106 private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache; 107 108 /** 109 * A request for contact details for the given number. 110 */ 111 private static final class ContactInfoRequest { 112 /** The number to look-up. */ 113 public final String number; 114 /** The country in which a call to or from this number was placed or received. */ 115 public final String countryIso; 116 /** The cached contact information stored in the call log. */ 117 public final ContactInfo callLogInfo; 118 119 public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) { 120 this.number = number; 121 this.countryIso = countryIso; 122 this.callLogInfo = callLogInfo; 123 } 124 125 @Override 126 public boolean equals(Object obj) { 127 if (this == obj) return true; 128 if (obj == null) return false; 129 if (!(obj instanceof ContactInfoRequest)) return false; 130 131 ContactInfoRequest other = (ContactInfoRequest) obj; 132 133 if (!TextUtils.equals(number, other.number)) return false; 134 if (!TextUtils.equals(countryIso, other.countryIso)) return false; 135 if (!Objects.equal(callLogInfo, other.callLogInfo)) return false; 136 137 return true; 138 } 139 140 @Override 141 public int hashCode() { 142 final int prime = 31; 143 int result = 1; 144 result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode()); 145 result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode()); 146 result = prime * result + ((number == null) ? 0 : number.hashCode()); 147 return result; 148 } 149 } 150 151 /** 152 * List of requests to update contact details. 153 * <p> 154 * Each request is made of a phone number to look up, and the contact info currently stored in 155 * the call log for this number. 156 * <p> 157 * The requests are added when displaying the contacts and are processed by a background 158 * thread. 159 */ 160 private final LinkedList<ContactInfoRequest> mRequests; 161 162 private volatile boolean mDone; 163 private boolean mLoading = true; 164 private ViewTreeObserver.OnPreDrawListener mPreDrawListener; 165 private static final int REDRAW = 1; 166 private static final int START_THREAD = 2; 167 168 private boolean mFirst; 169 private Thread mCallerIdThread; 170 171 /** Instance of helper class for managing views. */ 172 private final CallLogListItemHelper mCallLogViewsHelper; 173 174 /** Helper to set up contact photos. */ 175 private final ContactPhotoManager mContactPhotoManager; 176 /** Helper to parse and process phone numbers. */ 177 private PhoneNumberHelper mPhoneNumberHelper; 178 /** Helper to group call log entries. */ 179 private final CallLogGroupBuilder mCallLogGroupBuilder; 180 181 /** Can be set to true by tests to disable processing of requests. */ 182 private volatile boolean mRequestProcessingDisabled = false; 183 184 /** Listener for the primary action in the list, opens the call details. */ 185 private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() { 186 @Override 187 public void onClick(View view) { 188 IntentProvider intentProvider = (IntentProvider) view.getTag(); 189 if (intentProvider != null) { 190 mContext.startActivity(intentProvider.getIntent(mContext)); 191 } 192 } 193 }; 194 /** Listener for the secondary action in the list, either call or play. */ 195 private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() { 196 @Override 197 public void onClick(View view) { 198 IntentProvider intentProvider = (IntentProvider) view.getTag(); 199 if (intentProvider != null) { 200 mContext.startActivity(intentProvider.getIntent(mContext)); 201 } 202 } 203 }; 204 205 @Override 206 public boolean onPreDraw() { 207 if (mFirst) { 208 mHandler.sendEmptyMessageDelayed(START_THREAD, 209 START_PROCESSING_REQUESTS_DELAY_MILLIS); 210 mFirst = false; 211 } 212 return true; 213 } 214 215 private Handler mHandler = new Handler() { 216 @Override 217 public void handleMessage(Message msg) { 218 switch (msg.what) { 219 case REDRAW: 220 notifyDataSetChanged(); 221 break; 222 case START_THREAD: 223 startRequestProcessing(); 224 break; 225 } 226 } 227 }; 228 229 CallLogAdapter(Context context, CallFetcher callFetcher, 230 ContactInfoHelper contactInfoHelper) { 231 super(context); 232 233 mContext = context; 234 mCallFetcher = callFetcher; 235 mContactInfoHelper = contactInfoHelper; 236 237 mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); 238 mRequests = new LinkedList<ContactInfoRequest>(); 239 mPreDrawListener = null; 240 241 Resources resources = mContext.getResources(); 242 CallTypeHelper callTypeHelper = new CallTypeHelper(resources); 243 244 mContactPhotoManager = ContactPhotoManager.getInstance(mContext); 245 mPhoneNumberHelper = new PhoneNumberHelper(resources); 246 PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper( 247 resources, callTypeHelper, mPhoneNumberHelper); 248 mCallLogViewsHelper = 249 new CallLogListItemHelper( 250 phoneCallDetailsHelper, mPhoneNumberHelper, resources); 251 mCallLogGroupBuilder = new CallLogGroupBuilder(this); 252 } 253 254 /** 255 * Requery on background thread when {@link Cursor} changes. 256 */ 257 @Override 258 protected void onContentChanged() { 259 mCallFetcher.fetchCalls(); 260 } 261 262 void setLoading(boolean loading) { 263 mLoading = loading; 264 } 265 266 @Override 267 public boolean isEmpty() { 268 if (mLoading) { 269 // We don't want the empty state to show when loading. 270 return false; 271 } else { 272 return super.isEmpty(); 273 } 274 } 275 276 private void startRequestProcessing() { 277 if (mRequestProcessingDisabled) { 278 return; 279 } 280 281 mDone = false; 282 mCallerIdThread = new Thread(this, "CallLogContactLookup"); 283 mCallerIdThread.setPriority(Thread.MIN_PRIORITY); 284 mCallerIdThread.start(); 285 } 286 287 /** 288 * Stops the background thread that processes updates and cancels any pending requests to 289 * start it. 290 * <p> 291 * Should be called from the main thread to prevent a race condition between the request to 292 * start the thread being processed and stopping the thread. 293 */ 294 public void stopRequestProcessing() { 295 // Remove any pending requests to start the processing thread. 296 mHandler.removeMessages(START_THREAD); 297 mDone = true; 298 if (mCallerIdThread != null) mCallerIdThread.interrupt(); 299 } 300 301 public void invalidateCache() { 302 mContactInfoCache.expireAll(); 303 // Let it restart the thread after next draw 304 mPreDrawListener = null; 305 } 306 307 /** 308 * Enqueues a request to look up the contact details for the given phone number. 309 * <p> 310 * It also provides the current contact info stored in the call log for this number. 311 * <p> 312 * If the {@code immediate} parameter is true, it will start immediately the thread that looks 313 * up the contact information (if it has not been already started). Otherwise, it will be 314 * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}. 315 */ 316 @VisibleForTesting 317 void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, 318 boolean immediate) { 319 ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo); 320 synchronized (mRequests) { 321 if (!mRequests.contains(request)) { 322 mRequests.add(request); 323 mRequests.notifyAll(); 324 } 325 } 326 if (mFirst && immediate) { 327 startRequestProcessing(); 328 mFirst = false; 329 } 330 } 331 332 /** 333 * Queries the appropriate content provider for the contact associated with the number. 334 * <p> 335 * Upon completion it also updates the cache in the call log, if it is different from 336 * {@code callLogInfo}. 337 * <p> 338 * The number might be either a SIP address or a phone number. 339 * <p> 340 * It returns true if it updated the content of the cache and we should therefore tell the 341 * view to update its content. 342 */ 343 private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) { 344 final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso); 345 346 if (info == null) { 347 // The lookup failed, just return without requesting to update the view. 348 return false; 349 } 350 351 // Check the existing entry in the cache: only if it has changed we should update the 352 // view. 353 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 354 ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso); 355 boolean updated = !info.equals(existingInfo); 356 // Store the data in the cache so that the UI thread can use to display it. Store it 357 // even if it has not changed so that it is marked as not expired. 358 mContactInfoCache.put(numberCountryIso, info); 359 // Update the call log even if the cache it is up-to-date: it is possible that the cache 360 // contains the value from a different call log entry. 361 updateCallLogContactInfoCache(number, countryIso, info, callLogInfo); 362 return updated; 363 } 364 /* 365 * Handles requests for contact name and number type 366 * @see java.lang.Runnable#run() 367 */ 368 @Override 369 public void run() { 370 boolean needNotify = false; 371 while (!mDone) { 372 ContactInfoRequest request = null; 373 synchronized (mRequests) { 374 if (!mRequests.isEmpty()) { 375 request = mRequests.removeFirst(); 376 } else { 377 if (needNotify) { 378 needNotify = false; 379 mHandler.sendEmptyMessage(REDRAW); 380 } 381 try { 382 mRequests.wait(1000); 383 } catch (InterruptedException ie) { 384 // Ignore and continue processing requests 385 Thread.currentThread().interrupt(); 386 } 387 } 388 } 389 if (!mDone && request != null 390 && queryContactInfo(request.number, request.countryIso, request.callLogInfo)) { 391 needNotify = true; 392 } 393 } 394 } 395 396 @Override 397 protected void addGroups(Cursor cursor) { 398 mCallLogGroupBuilder.addGroups(cursor); 399 } 400 401 @Override 402 protected View newStandAloneView(Context context, ViewGroup parent) { 403 LayoutInflater inflater = 404 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 405 View view = inflater.inflate(R.layout.call_log_list_item, parent, false); 406 findAndCacheViews(view); 407 return view; 408 } 409 410 @Override 411 protected void bindStandAloneView(View view, Context context, Cursor cursor) { 412 bindView(view, cursor, 1); 413 } 414 415 @Override 416 protected View newChildView(Context context, ViewGroup parent) { 417 LayoutInflater inflater = 418 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 419 View view = inflater.inflate(R.layout.call_log_list_item, parent, false); 420 findAndCacheViews(view); 421 return view; 422 } 423 424 @Override 425 protected void bindChildView(View view, Context context, Cursor cursor) { 426 bindView(view, cursor, 1); 427 } 428 429 @Override 430 protected View newGroupView(Context context, ViewGroup parent) { 431 LayoutInflater inflater = 432 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 433 View view = inflater.inflate(R.layout.call_log_list_item, parent, false); 434 findAndCacheViews(view); 435 return view; 436 } 437 438 @Override 439 protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize, 440 boolean expanded) { 441 bindView(view, cursor, groupSize); 442 } 443 444 private void findAndCacheViews(View view) { 445 // Get the views to bind to. 446 CallLogListItemViews views = CallLogListItemViews.fromView(view); 447 views.primaryActionView.setOnClickListener(mPrimaryActionListener); 448 views.secondaryActionView.setOnClickListener(mSecondaryActionListener); 449 view.setTag(views); 450 } 451 452 /** 453 * Binds the views in the entry to the data in the call log. 454 * 455 * @param view the view corresponding to this entry 456 * @param c the cursor pointing to the entry in the call log 457 * @param count the number of entries in the current item, greater than 1 if it is a group 458 */ 459 private void bindView(View view, Cursor c, int count) { 460 final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); 461 final int section = c.getInt(CallLogQuery.SECTION); 462 463 // This might be a header: check the value of the section column in the cursor. 464 if (section == CallLogQuery.SECTION_NEW_HEADER 465 || section == CallLogQuery.SECTION_OLD_HEADER) { 466 views.primaryActionView.setVisibility(View.GONE); 467 views.bottomDivider.setVisibility(View.GONE); 468 views.listHeaderTextView.setVisibility(View.VISIBLE); 469 views.listHeaderTextView.setText( 470 section == CallLogQuery.SECTION_NEW_HEADER 471 ? R.string.call_log_new_header 472 : R.string.call_log_old_header); 473 // Nothing else to set up for a header. 474 return; 475 } 476 // Default case: an item in the call log. 477 views.primaryActionView.setVisibility(View.VISIBLE); 478 views.bottomDivider.setVisibility(isLastOfSection(c) ? View.GONE : View.VISIBLE); 479 views.listHeaderTextView.setVisibility(View.GONE); 480 481 final String number = c.getString(CallLogQuery.NUMBER); 482 final long date = c.getLong(CallLogQuery.DATE); 483 final long duration = c.getLong(CallLogQuery.DURATION); 484 final int callType = c.getInt(CallLogQuery.CALL_TYPE); 485 final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO); 486 487 final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c); 488 489 views.primaryActionView.setTag( 490 IntentProvider.getCallDetailIntentProvider( 491 this, c.getPosition(), c.getLong(CallLogQuery.ID), count)); 492 // Store away the voicemail information so we can play it directly. 493 if (callType == Calls.VOICEMAIL_TYPE) { 494 String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); 495 final long rowId = c.getLong(CallLogQuery.ID); 496 views.secondaryActionView.setTag( 497 IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri)); 498 } else if (!TextUtils.isEmpty(number)) { 499 // Store away the number so we can call it directly if you click on the call icon. 500 views.secondaryActionView.setTag( 501 IntentProvider.getReturnCallIntentProvider(number)); 502 } else { 503 // No action enabled. 504 views.secondaryActionView.setTag(null); 505 } 506 507 // Lookup contacts with this number 508 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 509 ExpirableCache.CachedValue<ContactInfo> cachedInfo = 510 mContactInfoCache.getCachedValue(numberCountryIso); 511 ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); 512 if (!mPhoneNumberHelper.canPlaceCallsTo(number) 513 || mPhoneNumberHelper.isVoicemailNumber(number)) { 514 // If this is a number that cannot be dialed, there is no point in looking up a contact 515 // for it. 516 info = ContactInfo.EMPTY; 517 } else if (cachedInfo == null) { 518 mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY); 519 // Use the cached contact info from the call log. 520 info = cachedContactInfo; 521 // The db request should happen on a non-UI thread. 522 // Request the contact details immediately since they are currently missing. 523 enqueueRequest(number, countryIso, cachedContactInfo, true); 524 // We will format the phone number when we make the background request. 525 } else { 526 if (cachedInfo.isExpired()) { 527 // The contact info is no longer up to date, we should request it. However, we 528 // do not need to request them immediately. 529 enqueueRequest(number, countryIso, cachedContactInfo, false); 530 } else if (!callLogInfoMatches(cachedContactInfo, info)) { 531 // The call log information does not match the one we have, look it up again. 532 // We could simply update the call log directly, but that needs to be done in a 533 // background thread, so it is easier to simply request a new lookup, which will, as 534 // a side-effect, update the call log. 535 enqueueRequest(number, countryIso, cachedContactInfo, false); 536 } 537 538 if (info == ContactInfo.EMPTY) { 539 // Use the cached contact info from the call log. 540 info = cachedContactInfo; 541 } 542 } 543 544 final Uri lookupUri = info.lookupUri; 545 final String name = info.name; 546 final int ntype = info.type; 547 final String label = info.label; 548 final long photoId = info.photoId; 549 CharSequence formattedNumber = info.formattedNumber; 550 final int[] callTypes = getCallTypes(c, count); 551 final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION); 552 final PhoneCallDetails details; 553 if (TextUtils.isEmpty(name)) { 554 details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode, 555 callTypes, date, duration); 556 } else { 557 // We do not pass a photo id since we do not need the high-res picture. 558 details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode, 559 callTypes, date, duration, name, ntype, label, lookupUri, null); 560 } 561 562 final boolean isNew = c.getInt(CallLogQuery.IS_READ) == 0; 563 // New items also use the highlighted version of the text. 564 final boolean isHighlighted = isNew; 565 mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted); 566 setPhoto(views, photoId, lookupUri); 567 568 // Listen for the first draw 569 if (mPreDrawListener == null) { 570 mFirst = true; 571 mPreDrawListener = this; 572 view.getViewTreeObserver().addOnPreDrawListener(this); 573 } 574 } 575 576 /** Returns true if this is the last item of a section. */ 577 private boolean isLastOfSection(Cursor c) { 578 if (c.isLast()) return true; 579 final int section = c.getInt(CallLogQuery.SECTION); 580 if (!c.moveToNext()) return true; 581 final int nextSection = c.getInt(CallLogQuery.SECTION); 582 c.moveToPrevious(); 583 return section != nextSection; 584 } 585 586 /** Checks whether the contact info from the call log matches the one from the contacts db. */ 587 private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) { 588 // The call log only contains a subset of the fields in the contacts db. 589 // Only check those. 590 return TextUtils.equals(callLogInfo.name, info.name) 591 && callLogInfo.type == info.type 592 && TextUtils.equals(callLogInfo.label, info.label); 593 } 594 595 /** Stores the updated contact info in the call log if it is different from the current one. */ 596 private void updateCallLogContactInfoCache(String number, String countryIso, 597 ContactInfo updatedInfo, ContactInfo callLogInfo) { 598 final ContentValues values = new ContentValues(); 599 boolean needsUpdate = false; 600 601 if (callLogInfo != null) { 602 if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) { 603 values.put(Calls.CACHED_NAME, updatedInfo.name); 604 needsUpdate = true; 605 } 606 607 if (updatedInfo.type != callLogInfo.type) { 608 values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); 609 needsUpdate = true; 610 } 611 612 if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) { 613 values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); 614 needsUpdate = true; 615 } 616 if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) { 617 values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); 618 needsUpdate = true; 619 } 620 if (!TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) { 621 values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); 622 needsUpdate = true; 623 } 624 if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) { 625 values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); 626 needsUpdate = true; 627 } 628 if (updatedInfo.photoId != callLogInfo.photoId) { 629 values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); 630 needsUpdate = true; 631 } 632 if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) { 633 values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); 634 needsUpdate = true; 635 } 636 } else { 637 // No previous values, store all of them. 638 values.put(Calls.CACHED_NAME, updatedInfo.name); 639 values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); 640 values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); 641 values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); 642 values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); 643 values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); 644 values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); 645 values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); 646 needsUpdate = true; 647 } 648 649 if (!needsUpdate) { 650 return; 651 } 652 653 if (countryIso == null) { 654 mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values, 655 Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL", 656 new String[]{ number }); 657 } else { 658 mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values, 659 Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?", 660 new String[]{ number, countryIso }); 661 } 662 } 663 664 /** Returns the contact information as stored in the call log. */ 665 private ContactInfo getContactInfoFromCallLog(Cursor c) { 666 ContactInfo info = new ContactInfo(); 667 info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI)); 668 info.name = c.getString(CallLogQuery.CACHED_NAME); 669 info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE); 670 info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL); 671 String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER); 672 info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber; 673 info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER); 674 info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID); 675 info.photoUri = null; // We do not cache the photo URI. 676 info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER); 677 return info; 678 } 679 680 /** 681 * Returns the call types for the given number of items in the cursor. 682 * <p> 683 * It uses the next {@code count} rows in the cursor to extract the types. 684 * <p> 685 * It position in the cursor is unchanged by this function. 686 */ 687 private int[] getCallTypes(Cursor cursor, int count) { 688 int position = cursor.getPosition(); 689 int[] callTypes = new int[count]; 690 for (int index = 0; index < count; ++index) { 691 callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); 692 cursor.moveToNext(); 693 } 694 cursor.moveToPosition(position); 695 return callTypes; 696 } 697 698 private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri) { 699 views.quickContactView.assignContactUri(contactUri); 700 mContactPhotoManager.loadPhoto(views.quickContactView, photoId, false, true); 701 } 702 703 /** 704 * Sets whether processing of requests for contact details should be enabled. 705 * <p> 706 * This method should be called in tests to disable such processing of requests when not 707 * needed. 708 */ 709 @VisibleForTesting 710 void disableRequestProcessingForTest() { 711 mRequestProcessingDisabled = true; 712 } 713 714 @VisibleForTesting 715 void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { 716 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 717 mContactInfoCache.put(numberCountryIso, contactInfo); 718 } 719 720 @Override 721 public void addGroup(int cursorPosition, int size, boolean expanded) { 722 super.addGroup(cursorPosition, size, expanded); 723 } 724 725 /* 726 * Get the number from the Contacts, if available, since sometimes 727 * the number provided by caller id may not be formatted properly 728 * depending on the carrier (roaming) in use at the time of the 729 * incoming call. 730 * Logic : If the caller-id number starts with a "+", use it 731 * Else if the number in the contacts starts with a "+", use that one 732 * Else if the number in the contacts is longer, use that one 733 */ 734 public String getBetterNumberFromContacts(String number, String countryIso) { 735 String matchingNumber = null; 736 // Look in the cache first. If it's not found then query the Phones db 737 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 738 ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso); 739 if (ci != null && ci != ContactInfo.EMPTY) { 740 matchingNumber = ci.number; 741 } else { 742 try { 743 Cursor phonesCursor = mContext.getContentResolver().query( 744 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number), 745 PhoneQuery._PROJECTION, null, null, null); 746 if (phonesCursor != null) { 747 if (phonesCursor.moveToFirst()) { 748 matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER); 749 } 750 phonesCursor.close(); 751 } 752 } catch (Exception e) { 753 // Use the number from the call log 754 } 755 } 756 if (!TextUtils.isEmpty(matchingNumber) && 757 (matchingNumber.startsWith("+") 758 || matchingNumber.length() > number.length())) { 759 number = matchingNumber; 760 } 761 return number; 762 } 763 } 764