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