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