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