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