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.dialer.calllog; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.res.Resources; 23 import android.database.Cursor; 24 import android.net.Uri; 25 import android.os.Handler; 26 import android.os.Message; 27 import android.provider.CallLog.Calls; 28 import android.provider.ContactsContract.PhoneLookup; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.ViewStub; 35 import android.view.ViewTreeObserver; 36 import android.widget.ImageView; 37 import android.widget.TextView; 38 39 import com.android.common.widget.GroupingListAdapter; 40 import com.android.contacts.common.ContactPhotoManager; 41 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; 42 import com.android.contacts.common.util.UriUtils; 43 import com.android.dialer.PhoneCallDetails; 44 import com.android.dialer.PhoneCallDetailsHelper; 45 import com.android.dialer.R; 46 import com.android.dialer.util.ExpirableCache; 47 48 import com.google.common.annotations.VisibleForTesting; 49 import com.google.common.base.Objects; 50 51 import java.util.LinkedList; 52 53 /** 54 * Adapter class to fill in data for the Call Log. 55 */ 56 public class CallLogAdapter extends GroupingListAdapter 57 implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator { 58 59 /** Interface used to initiate a refresh of the content. */ 60 public interface CallFetcher { 61 public void fetchCalls(); 62 } 63 64 /** 65 * Stores a phone number of a call with the country code where it originally occurred. 66 * <p> 67 * Note the country does not necessarily specifies the country of the phone number itself, but 68 * it is the country in which the user was in when the call was placed or received. 69 */ 70 private static final class NumberWithCountryIso { 71 public final String number; 72 public final String countryIso; 73 74 public NumberWithCountryIso(String number, String countryIso) { 75 this.number = number; 76 this.countryIso = countryIso; 77 } 78 79 @Override 80 public boolean equals(Object o) { 81 if (o == null) return false; 82 if (!(o instanceof NumberWithCountryIso)) return false; 83 NumberWithCountryIso other = (NumberWithCountryIso) o; 84 return TextUtils.equals(number, other.number) 85 && TextUtils.equals(countryIso, other.countryIso); 86 } 87 88 @Override 89 public int hashCode() { 90 return (number == null ? 0 : number.hashCode()) 91 ^ (countryIso == null ? 0 : countryIso.hashCode()); 92 } 93 } 94 95 /** The time in millis to delay starting the thread processing requests. */ 96 private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000; 97 98 /** The size of the cache of contact info. */ 99 private static final int CONTACT_INFO_CACHE_SIZE = 100; 100 101 protected final Context mContext; 102 private final ContactInfoHelper mContactInfoHelper; 103 private final CallFetcher mCallFetcher; 104 private ViewTreeObserver mViewTreeObserver = null; 105 106 /** 107 * A cache of the contact details for the phone numbers in the call log. 108 * <p> 109 * The content of the cache is expired (but not purged) whenever the application comes to 110 * the foreground. 111 * <p> 112 * The key is number with the country in which the call was placed or received. 113 */ 114 private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache; 115 116 /** 117 * A request for contact details for the given number. 118 */ 119 private static final class ContactInfoRequest { 120 /** The number to look-up. */ 121 public final String number; 122 /** The country in which a call to or from this number was placed or received. */ 123 public final String countryIso; 124 /** The cached contact information stored in the call log. */ 125 public final ContactInfo callLogInfo; 126 127 public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) { 128 this.number = number; 129 this.countryIso = countryIso; 130 this.callLogInfo = callLogInfo; 131 } 132 133 @Override 134 public boolean equals(Object obj) { 135 if (this == obj) return true; 136 if (obj == null) return false; 137 if (!(obj instanceof ContactInfoRequest)) return false; 138 139 ContactInfoRequest other = (ContactInfoRequest) obj; 140 141 if (!TextUtils.equals(number, other.number)) return false; 142 if (!TextUtils.equals(countryIso, other.countryIso)) return false; 143 if (!Objects.equal(callLogInfo, other.callLogInfo)) return false; 144 145 return true; 146 } 147 148 @Override 149 public int hashCode() { 150 final int prime = 31; 151 int result = 1; 152 result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode()); 153 result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode()); 154 result = prime * result + ((number == null) ? 0 : number.hashCode()); 155 return result; 156 } 157 } 158 159 /** 160 * List of requests to update contact details. 161 * <p> 162 * Each request is made of a phone number to look up, and the contact info currently stored in 163 * the call log for this number. 164 * <p> 165 * The requests are added when displaying the contacts and are processed by a background 166 * thread. 167 */ 168 private final LinkedList<ContactInfoRequest> mRequests; 169 170 private boolean mLoading = true; 171 private static final int REDRAW = 1; 172 private static final int START_THREAD = 2; 173 174 private QueryThread mCallerIdThread; 175 176 /** Instance of helper class for managing views. */ 177 private final CallLogListItemHelper mCallLogViewsHelper; 178 179 /** Helper to set up contact photos. */ 180 private final ContactPhotoManager mContactPhotoManager; 181 /** Helper to parse and process phone numbers. */ 182 private PhoneNumberDisplayHelper mPhoneNumberHelper; 183 /** Helper to group call log entries. */ 184 private final CallLogGroupBuilder mCallLogGroupBuilder; 185 186 /** Can be set to true by tests to disable processing of requests. */ 187 private volatile boolean mRequestProcessingDisabled = false; 188 189 /** 190 * Whether to show the secondary action button used to play voicemail or show call details. 191 * True if created from a CallLogFragment. 192 * False if created from the PhoneFavoriteFragment. */ 193 private boolean mShowSecondaryActionButton = true; 194 195 private boolean mIsCallLog = true; 196 private int mNumMissedCalls = 0; 197 private int mNumMissedCallsShown = 0; 198 199 private View mBadgeContainer; 200 private ImageView mBadgeImageView; 201 private TextView mBadgeText; 202 203 /** Listener for the primary or secondary actions in the list. 204 * Primary opens the call details. 205 * Secondary calls or plays. 206 **/ 207 private final View.OnClickListener mActionListener = new View.OnClickListener() { 208 @Override 209 public void onClick(View view) { 210 startActivityForAction(view); 211 } 212 }; 213 214 private void startActivityForAction(View view) { 215 final IntentProvider intentProvider = (IntentProvider) view.getTag(); 216 if (intentProvider != null) { 217 final Intent intent = intentProvider.getIntent(mContext); 218 // See IntentProvider.getCallDetailIntentProvider() for why this may be null. 219 if (intent != null) { 220 mContext.startActivity(intent); 221 } 222 } 223 } 224 225 @Override 226 public boolean onPreDraw() { 227 // We only wanted to listen for the first draw (and this is it). 228 unregisterPreDrawListener(); 229 230 // Only schedule a thread-creation message if the thread hasn't been 231 // created yet. This is purely an optimization, to queue fewer messages. 232 if (mCallerIdThread == null) { 233 mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS); 234 } 235 236 return true; 237 } 238 239 private Handler mHandler = new Handler() { 240 @Override 241 public void handleMessage(Message msg) { 242 switch (msg.what) { 243 case REDRAW: 244 notifyDataSetChanged(); 245 break; 246 case START_THREAD: 247 startRequestProcessing(); 248 break; 249 } 250 } 251 }; 252 253 public CallLogAdapter(Context context, CallFetcher callFetcher, 254 ContactInfoHelper contactInfoHelper, boolean showSecondaryActionButton, 255 boolean isCallLog) { 256 super(context); 257 258 mContext = context; 259 mCallFetcher = callFetcher; 260 mContactInfoHelper = contactInfoHelper; 261 mShowSecondaryActionButton = showSecondaryActionButton; 262 mIsCallLog = isCallLog; 263 264 mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); 265 mRequests = new LinkedList<ContactInfoRequest>(); 266 267 Resources resources = mContext.getResources(); 268 CallTypeHelper callTypeHelper = new CallTypeHelper(resources); 269 270 mContactPhotoManager = ContactPhotoManager.getInstance(mContext); 271 mPhoneNumberHelper = new PhoneNumberDisplayHelper(resources); 272 PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper( 273 resources, callTypeHelper, new PhoneNumberUtilsWrapper()); 274 mCallLogViewsHelper = 275 new CallLogListItemHelper( 276 phoneCallDetailsHelper, mPhoneNumberHelper, resources); 277 mCallLogGroupBuilder = new CallLogGroupBuilder(this); 278 } 279 280 /** 281 * Requery on background thread when {@link Cursor} changes. 282 */ 283 @Override 284 protected void onContentChanged() { 285 mCallFetcher.fetchCalls(); 286 } 287 288 public void setLoading(boolean loading) { 289 mLoading = loading; 290 } 291 292 @Override 293 public boolean isEmpty() { 294 if (mLoading) { 295 // We don't want the empty state to show when loading. 296 return false; 297 } else { 298 return super.isEmpty(); 299 } 300 } 301 302 /** 303 * Starts a background thread to process contact-lookup requests, unless one 304 * has already been started. 305 */ 306 private synchronized void startRequestProcessing() { 307 // For unit-testing. 308 if (mRequestProcessingDisabled) return; 309 310 // Idempotence... if a thread is already started, don't start another. 311 if (mCallerIdThread != null) return; 312 313 mCallerIdThread = new QueryThread(); 314 mCallerIdThread.setPriority(Thread.MIN_PRIORITY); 315 mCallerIdThread.start(); 316 } 317 318 /** 319 * Stops the background thread that processes updates and cancels any 320 * pending requests to start it. 321 */ 322 public synchronized void stopRequestProcessing() { 323 // Remove any pending requests to start the processing thread. 324 mHandler.removeMessages(START_THREAD); 325 if (mCallerIdThread != null) { 326 // Stop the thread; we are finished with it. 327 mCallerIdThread.stopProcessing(); 328 mCallerIdThread.interrupt(); 329 mCallerIdThread = null; 330 } 331 } 332 333 /** 334 * Stop receiving onPreDraw() notifications. 335 */ 336 private void unregisterPreDrawListener() { 337 if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) { 338 mViewTreeObserver.removeOnPreDrawListener(this); 339 } 340 mViewTreeObserver = null; 341 } 342 343 public void invalidateCache() { 344 mContactInfoCache.expireAll(); 345 346 // Restart the request-processing thread after the next draw. 347 stopRequestProcessing(); 348 unregisterPreDrawListener(); 349 } 350 351 /** 352 * Enqueues a request to look up the contact details for the given phone number. 353 * <p> 354 * It also provides the current contact info stored in the call log for this number. 355 * <p> 356 * If the {@code immediate} parameter is true, it will start immediately the thread that looks 357 * up the contact information (if it has not been already started). Otherwise, it will be 358 * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}. 359 */ 360 protected void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, 361 boolean immediate) { 362 ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo); 363 synchronized (mRequests) { 364 if (!mRequests.contains(request)) { 365 mRequests.add(request); 366 mRequests.notifyAll(); 367 } 368 } 369 if (immediate) startRequestProcessing(); 370 } 371 372 /** 373 * Queries the appropriate content provider for the contact associated with the number. 374 * <p> 375 * Upon completion it also updates the cache in the call log, if it is different from 376 * {@code callLogInfo}. 377 * <p> 378 * The number might be either a SIP address or a phone number. 379 * <p> 380 * It returns true if it updated the content of the cache and we should therefore tell the 381 * view to update its content. 382 */ 383 private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) { 384 final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso); 385 386 if (info == null) { 387 // The lookup failed, just return without requesting to update the view. 388 return false; 389 } 390 391 // Check the existing entry in the cache: only if it has changed we should update the 392 // view. 393 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 394 ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso); 395 396 final boolean isRemoteSource = info.sourceType != 0; 397 398 // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY} 399 // to avoid updating the data set for every new row that is scrolled into view. 400 // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/) 401 402 // Exception: Photo uris for contacts from remote sources are not cached in the call log 403 // cache, so we have to force a redraw for these contacts regardless. 404 boolean updated = (existingInfo != ContactInfo.EMPTY || isRemoteSource) && 405 !info.equals(existingInfo); 406 407 // Store the data in the cache so that the UI thread can use to display it. Store it 408 // even if it has not changed so that it is marked as not expired. 409 mContactInfoCache.put(numberCountryIso, info); 410 // Update the call log even if the cache it is up-to-date: it is possible that the cache 411 // contains the value from a different call log entry. 412 updateCallLogContactInfoCache(number, countryIso, info, callLogInfo); 413 return updated; 414 } 415 416 /* 417 * Handles requests for contact name and number type. 418 */ 419 private class QueryThread extends Thread { 420 private volatile boolean mDone = false; 421 422 public QueryThread() { 423 super("CallLogAdapter.QueryThread"); 424 } 425 426 public void stopProcessing() { 427 mDone = true; 428 } 429 430 @Override 431 public void run() { 432 boolean needRedraw = false; 433 while (true) { 434 // Check if thread is finished, and if so return immediately. 435 if (mDone) return; 436 437 // Obtain next request, if any is available. 438 // Keep synchronized section small. 439 ContactInfoRequest req = null; 440 synchronized (mRequests) { 441 if (!mRequests.isEmpty()) { 442 req = mRequests.removeFirst(); 443 } 444 } 445 446 if (req != null) { 447 // Process the request. If the lookup succeeds, schedule a 448 // redraw. 449 needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo); 450 } else { 451 // Throttle redraw rate by only sending them when there are 452 // more requests. 453 if (needRedraw) { 454 needRedraw = false; 455 mHandler.sendEmptyMessage(REDRAW); 456 } 457 458 // Wait until another request is available, or until this 459 // thread is no longer needed (as indicated by being 460 // interrupted). 461 try { 462 synchronized (mRequests) { 463 mRequests.wait(1000); 464 } 465 } catch (InterruptedException ie) { 466 // Ignore, and attempt to continue processing requests. 467 } 468 } 469 } 470 } 471 } 472 473 @Override 474 protected void addGroups(Cursor cursor) { 475 mCallLogGroupBuilder.addGroups(cursor); 476 } 477 478 @Override 479 protected View newStandAloneView(Context context, ViewGroup parent) { 480 return newChildView(context, parent); 481 } 482 483 @Override 484 protected View newGroupView(Context context, ViewGroup parent) { 485 return newChildView(context, parent); 486 } 487 488 @Override 489 protected View newChildView(Context context, ViewGroup parent) { 490 LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 491 View view = inflater.inflate(R.layout.call_log_list_item, parent, false); 492 findAndCacheViews(view); 493 return view; 494 } 495 496 @Override 497 protected void bindStandAloneView(View view, Context context, Cursor cursor) { 498 bindView(view, cursor, 1); 499 } 500 501 @Override 502 protected void bindChildView(View view, Context context, Cursor cursor) { 503 bindView(view, cursor, 1); 504 } 505 506 @Override 507 protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize, 508 boolean expanded) { 509 bindView(view, cursor, groupSize); 510 } 511 512 private void findAndCacheViews(View view) { 513 // Get the views to bind to. 514 CallLogListItemViews views = CallLogListItemViews.fromView(view); 515 views.primaryActionView.setOnClickListener(mActionListener); 516 views.secondaryActionButtonView.setOnClickListener(mActionListener); 517 view.setTag(views); 518 } 519 520 /** 521 * Binds the views in the entry to the data in the call log. 522 * 523 * @param view the view corresponding to this entry 524 * @param c the cursor pointing to the entry in the call log 525 * @param count the number of entries in the current item, greater than 1 if it is a group 526 */ 527 private void bindView(View view, Cursor c, int count) { 528 final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); 529 530 // Default case: an item in the call log. 531 views.primaryActionView.setVisibility(View.VISIBLE); 532 views.listHeaderTextView.setVisibility(View.GONE); 533 534 final String number = c.getString(CallLogQuery.NUMBER); 535 final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION); 536 final long date = c.getLong(CallLogQuery.DATE); 537 final long duration = c.getLong(CallLogQuery.DURATION); 538 final int callType = c.getInt(CallLogQuery.CALL_TYPE); 539 final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO); 540 541 final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c); 542 543 final boolean isVoicemailNumber = 544 PhoneNumberUtilsWrapper.INSTANCE.isVoicemailNumber(number); 545 546 // Primary action is always to call, if possible. 547 if (PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) { 548 // Sets the primary action to call the number. 549 views.primaryActionView.setTag(IntentProvider.getReturnCallIntentProvider(number)); 550 } else { 551 views.primaryActionView.setTag(null); 552 } 553 554 if ( mShowSecondaryActionButton ) { 555 // Store away the voicemail information so we can play it directly. 556 if (callType == Calls.VOICEMAIL_TYPE) { 557 String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); 558 final long rowId = c.getLong(CallLogQuery.ID); 559 views.secondaryActionButtonView.setTag( 560 IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri)); 561 } else { 562 // Store the call details information. 563 views.secondaryActionButtonView.setTag( 564 IntentProvider.getCallDetailIntentProvider( 565 getCursor(), c.getPosition(), c.getLong(CallLogQuery.ID), count)); 566 } 567 } else { 568 // No action enabled. 569 views.secondaryActionButtonView.setTag(null); 570 } 571 572 // Lookup contacts with this number 573 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 574 ExpirableCache.CachedValue<ContactInfo> cachedInfo = 575 mContactInfoCache.getCachedValue(numberCountryIso); 576 ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); 577 if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation) 578 || isVoicemailNumber) { 579 // If this is a number that cannot be dialed, there is no point in looking up a contact 580 // for it. 581 info = ContactInfo.EMPTY; 582 } else if (cachedInfo == null) { 583 mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY); 584 // Use the cached contact info from the call log. 585 info = cachedContactInfo; 586 // The db request should happen on a non-UI thread. 587 // Request the contact details immediately since they are currently missing. 588 enqueueRequest(number, countryIso, cachedContactInfo, true); 589 // We will format the phone number when we make the background request. 590 } else { 591 if (cachedInfo.isExpired()) { 592 // The contact info is no longer up to date, we should request it. However, we 593 // do not need to request them immediately. 594 enqueueRequest(number, countryIso, cachedContactInfo, false); 595 } else if (!callLogInfoMatches(cachedContactInfo, info)) { 596 // The call log information does not match the one we have, look it up again. 597 // We could simply update the call log directly, but that needs to be done in a 598 // background thread, so it is easier to simply request a new lookup, which will, as 599 // a side-effect, update the call log. 600 enqueueRequest(number, countryIso, cachedContactInfo, false); 601 } 602 603 if (info == ContactInfo.EMPTY) { 604 // Use the cached contact info from the call log. 605 info = cachedContactInfo; 606 } 607 } 608 609 final Uri lookupUri = info.lookupUri; 610 final String name = info.name; 611 final int ntype = info.type; 612 final String label = info.label; 613 final long photoId = info.photoId; 614 final Uri photoUri = info.photoUri; 615 CharSequence formattedNumber = info.formattedNumber; 616 final int[] callTypes = getCallTypes(c, count); 617 final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION); 618 final int sourceType = info.sourceType; 619 final PhoneCallDetails details; 620 621 if (TextUtils.isEmpty(name)) { 622 details = new PhoneCallDetails(number, numberPresentation, 623 formattedNumber, countryIso, geocode, callTypes, date, 624 duration); 625 } else { 626 details = new PhoneCallDetails(number, numberPresentation, 627 formattedNumber, countryIso, geocode, callTypes, date, 628 duration, name, ntype, label, lookupUri, photoUri, sourceType); 629 } 630 631 final boolean isNew = c.getInt(CallLogQuery.IS_READ) == 0; 632 // New items also use the highlighted version of the text. 633 final boolean isHighlighted = isNew; 634 mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted, 635 mShowSecondaryActionButton); 636 637 int contactType = ContactPhotoManager.TYPE_DEFAULT; 638 639 if (isVoicemailNumber) { 640 contactType = ContactPhotoManager.TYPE_VOICEMAIL; 641 } else if (mContactInfoHelper.isBusiness(info.sourceType)) { 642 contactType = ContactPhotoManager.TYPE_BUSINESS; 643 } 644 645 String lookupKey = lookupUri == null ? null 646 : ContactInfoHelper.getLookupKeyFromUri(lookupUri); 647 648 String nameForDefaultImage = null; 649 if (TextUtils.isEmpty(name)) { 650 nameForDefaultImage = mPhoneNumberHelper.getDisplayNumber(details.number, 651 details.numberPresentation, details.formattedNumber).toString(); 652 } else { 653 nameForDefaultImage = name; 654 } 655 656 if (photoId == 0 && photoUri != null) { 657 setPhoto(views, photoUri, lookupUri, nameForDefaultImage, lookupKey, contactType); 658 } else { 659 setPhoto(views, photoId, lookupUri, nameForDefaultImage, lookupKey, contactType); 660 } 661 662 // Listen for the first draw 663 if (mViewTreeObserver == null) { 664 mViewTreeObserver = view.getViewTreeObserver(); 665 mViewTreeObserver.addOnPreDrawListener(this); 666 } 667 668 bindBadge(view, info, details, callType); 669 } 670 671 protected void bindBadge(View view, ContactInfo info, PhoneCallDetails details, int callType) { 672 673 // Do not show badge in call log. 674 if (!mIsCallLog) { 675 final int numMissed = getNumMissedCalls(callType); 676 final ViewStub stub = (ViewStub) view.findViewById(R.id.link_stub); 677 678 if (shouldShowBadge(numMissed, info, details)) { 679 // Do not process if the data has not changed (optimization since bind view is 680 // called multiple times due to contact lookup). 681 if (numMissed == mNumMissedCallsShown) { 682 return; 683 } 684 685 // stub will be null if it was already inflated. 686 if (stub != null) { 687 final View inflated = stub.inflate(); 688 inflated.setVisibility(View.VISIBLE); 689 mBadgeContainer = inflated.findViewById(R.id.badge_link_container); 690 mBadgeImageView = (ImageView) inflated.findViewById(R.id.badge_image); 691 mBadgeText = (TextView) inflated.findViewById(R.id.badge_text); 692 } 693 694 mBadgeContainer.setOnClickListener(getBadgeClickListener()); 695 mBadgeImageView.setImageResource(getBadgeImageResId()); 696 mBadgeText.setText(getBadgeText(numMissed)); 697 698 mNumMissedCallsShown = numMissed; 699 } else { 700 // Hide badge if it was previously shown. 701 if (stub == null) { 702 final View container = view.findViewById(R.id.badge_container); 703 if (container != null) { 704 container.setVisibility(View.GONE); 705 } 706 } 707 } 708 } 709 } 710 711 public void setMissedCalls(Cursor data) { 712 final int missed; 713 if (data == null) { 714 missed = 0; 715 } else { 716 missed = data.getCount(); 717 } 718 // Only need to update if the number of calls changed. 719 if (missed != mNumMissedCalls) { 720 mNumMissedCalls = missed; 721 notifyDataSetChanged(); 722 } 723 } 724 725 protected View.OnClickListener getBadgeClickListener() { 726 return new View.OnClickListener() { 727 @Override 728 public void onClick(View v) { 729 final Intent intent = new Intent(mContext, CallLogActivity.class); 730 mContext.startActivity(intent); 731 } 732 }; 733 } 734 735 /** 736 * Get the resource id for the image to be shown for the badge. 737 */ 738 protected int getBadgeImageResId() { 739 return R.drawable.ic_call_log_blue; 740 } 741 742 /** 743 * Get the text to be shown for the badge. 744 * 745 * @param numMissed The number of missed calls. 746 */ 747 protected String getBadgeText(int numMissed) { 748 return mContext.getResources().getString(R.string.num_missed_calls, numMissed); 749 } 750 751 /** 752 * Whether to show the badge. 753 * 754 * @param numMissedCalls The number of missed calls. 755 * @param info The contact info. 756 * @param details The call detail. 757 * @return {@literal true} if badge should be shown. {@literal false} otherwise. 758 */ 759 protected boolean shouldShowBadge(int numMissedCalls, ContactInfo info, 760 PhoneCallDetails details) { 761 return numMissedCalls > 0; 762 } 763 764 private int getNumMissedCalls(int callType) { 765 if (callType == Calls.MISSED_TYPE) { 766 // Exclude the current missed call shown in the shortcut. 767 return mNumMissedCalls - 1; 768 } 769 return mNumMissedCalls; 770 } 771 772 /** Checks whether the contact info from the call log matches the one from the contacts db. */ 773 private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) { 774 // The call log only contains a subset of the fields in the contacts db. 775 // Only check those. 776 return TextUtils.equals(callLogInfo.name, info.name) 777 && callLogInfo.type == info.type 778 && TextUtils.equals(callLogInfo.label, info.label); 779 } 780 781 /** Stores the updated contact info in the call log if it is different from the current one. */ 782 private void updateCallLogContactInfoCache(String number, String countryIso, 783 ContactInfo updatedInfo, ContactInfo callLogInfo) { 784 final ContentValues values = new ContentValues(); 785 boolean needsUpdate = false; 786 787 if (callLogInfo != null) { 788 if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) { 789 values.put(Calls.CACHED_NAME, updatedInfo.name); 790 needsUpdate = true; 791 } 792 793 if (updatedInfo.type != callLogInfo.type) { 794 values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); 795 needsUpdate = true; 796 } 797 798 if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) { 799 values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); 800 needsUpdate = true; 801 } 802 if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) { 803 values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); 804 needsUpdate = true; 805 } 806 if (!TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) { 807 values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); 808 needsUpdate = true; 809 } 810 if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) { 811 values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); 812 needsUpdate = true; 813 } 814 if (updatedInfo.photoId != callLogInfo.photoId) { 815 values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); 816 needsUpdate = true; 817 } 818 if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) { 819 values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); 820 needsUpdate = true; 821 } 822 } else { 823 // No previous values, store all of them. 824 values.put(Calls.CACHED_NAME, updatedInfo.name); 825 values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); 826 values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); 827 values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); 828 values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); 829 values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); 830 values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); 831 values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); 832 needsUpdate = true; 833 } 834 835 if (!needsUpdate) return; 836 837 if (countryIso == null) { 838 mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values, 839 Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL", 840 new String[]{ number }); 841 } else { 842 mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values, 843 Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?", 844 new String[]{ number, countryIso }); 845 } 846 } 847 848 /** Returns the contact information as stored in the call log. */ 849 private ContactInfo getContactInfoFromCallLog(Cursor c) { 850 ContactInfo info = new ContactInfo(); 851 info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI)); 852 info.name = c.getString(CallLogQuery.CACHED_NAME); 853 info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE); 854 info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL); 855 String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER); 856 info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber; 857 info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER); 858 info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID); 859 info.photoUri = null; // We do not cache the photo URI. 860 info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER); 861 return info; 862 } 863 864 /** 865 * Returns the call types for the given number of items in the cursor. 866 * <p> 867 * It uses the next {@code count} rows in the cursor to extract the types. 868 * <p> 869 * It position in the cursor is unchanged by this function. 870 */ 871 private int[] getCallTypes(Cursor cursor, int count) { 872 int position = cursor.getPosition(); 873 int[] callTypes = new int[count]; 874 for (int index = 0; index < count; ++index) { 875 callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); 876 cursor.moveToNext(); 877 } 878 cursor.moveToPosition(position); 879 return callTypes; 880 } 881 882 private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri, 883 String displayName, String identifier, int contactType) { 884 views.quickContactView.assignContactUri(contactUri); 885 DefaultImageRequest request = new DefaultImageRequest(displayName, identifier, 886 contactType); 887 mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, false /* darkTheme */, 888 request); 889 } 890 891 private void setPhoto(CallLogListItemViews views, Uri photoUri, Uri contactUri, 892 String displayName, String identifier, int contactType) { 893 views.quickContactView.assignContactUri(contactUri); 894 DefaultImageRequest request = new DefaultImageRequest(displayName, identifier, 895 contactType); 896 mContactPhotoManager.loadDirectoryPhoto(views.quickContactView, photoUri, 897 false /* darkTheme */, request); 898 } 899 900 901 /** 902 * Sets whether processing of requests for contact details should be enabled. 903 * <p> 904 * This method should be called in tests to disable such processing of requests when not 905 * needed. 906 */ 907 @VisibleForTesting 908 void disableRequestProcessingForTest() { 909 mRequestProcessingDisabled = true; 910 } 911 912 @VisibleForTesting 913 void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { 914 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 915 mContactInfoCache.put(numberCountryIso, contactInfo); 916 } 917 918 @Override 919 public void addGroup(int cursorPosition, int size, boolean expanded) { 920 super.addGroup(cursorPosition, size, expanded); 921 } 922 923 /* 924 * Get the number from the Contacts, if available, since sometimes 925 * the number provided by caller id may not be formatted properly 926 * depending on the carrier (roaming) in use at the time of the 927 * incoming call. 928 * Logic : If the caller-id number starts with a "+", use it 929 * Else if the number in the contacts starts with a "+", use that one 930 * Else if the number in the contacts is longer, use that one 931 */ 932 public String getBetterNumberFromContacts(String number, String countryIso) { 933 String matchingNumber = null; 934 // Look in the cache first. If it's not found then query the Phones db 935 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 936 ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso); 937 if (ci != null && ci != ContactInfo.EMPTY) { 938 matchingNumber = ci.number; 939 } else { 940 try { 941 Cursor phonesCursor = mContext.getContentResolver().query( 942 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number), 943 PhoneQuery._PROJECTION, null, null, null); 944 if (phonesCursor != null) { 945 if (phonesCursor.moveToFirst()) { 946 matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER); 947 } 948 phonesCursor.close(); 949 } 950 } catch (Exception e) { 951 // Use the number from the call log 952 } 953 } 954 if (!TextUtils.isEmpty(matchingNumber) && 955 (matchingNumber.startsWith("+") 956 || matchingNumber.length() > number.length())) { 957 number = matchingNumber; 958 } 959 return number; 960 } 961 } 962