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.graphics.drawable.Drawable; 25 import android.net.Uri; 26 import android.os.Handler; 27 import android.os.Message; 28 import android.provider.CallLog.Calls; 29 import android.provider.ContactsContract.PhoneLookup; 30 import android.telecom.PhoneAccountHandle; 31 import android.text.TextUtils; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.View.AccessibilityDelegate; 35 import android.view.ViewGroup; 36 import android.view.ViewStub; 37 import android.view.ViewTreeObserver; 38 import android.view.accessibility.AccessibilityEvent; 39 import android.widget.ImageView; 40 import android.widget.TextView; 41 import android.widget.Toast; 42 43 import com.android.common.widget.GroupingListAdapter; 44 import com.android.contacts.common.CallUtil; 45 import com.android.contacts.common.ContactPhotoManager; 46 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; 47 import com.android.contacts.common.util.UriUtils; 48 import com.android.dialer.DialtactsActivity; 49 import com.android.dialer.PhoneCallDetails; 50 import com.android.dialer.PhoneCallDetailsHelper; 51 import com.android.dialer.R; 52 import com.android.dialer.util.DialerUtils; 53 import com.android.dialer.util.ExpirableCache; 54 55 import com.google.common.annotations.VisibleForTesting; 56 import com.google.common.base.Objects; 57 58 import java.util.HashMap; 59 import java.util.LinkedList; 60 61 /** 62 * Adapter class to fill in data for the Call Log. 63 */ 64 public class CallLogAdapter extends GroupingListAdapter 65 implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator { 66 67 private static final int VOICEMAIL_TRANSCRIPTION_MAX_LINES = 10; 68 69 /** The enumeration of {@link android.os.AsyncTask} objects used in this class. */ 70 public enum Tasks { 71 REMOVE_CALL_LOG_ENTRIES, 72 } 73 74 /** Interface used to inform a parent UI element that a list item has been expanded. */ 75 public interface CallItemExpandedListener { 76 /** 77 * @param view The {@link CallLogListItemView} that represents the item that was clicked 78 * on. 79 */ onItemExpanded(CallLogListItemView view)80 public void onItemExpanded(CallLogListItemView view); 81 82 /** 83 * Retrieves the call log view for the specified call Id. If the view is not currently 84 * visible, returns null. 85 * 86 * @param callId The call Id. 87 * @return The call log view. 88 */ getViewForCallId(long callId)89 public CallLogListItemView getViewForCallId(long callId); 90 } 91 92 /** Interface used to initiate a refresh of the content. */ 93 public interface CallFetcher { fetchCalls()94 public void fetchCalls(); 95 } 96 97 /** Implements onClickListener for the report button. */ 98 public interface OnReportButtonClickListener { onReportButtonClick(String number)99 public void onReportButtonClick(String number); 100 } 101 102 /** 103 * Stores a phone number of a call with the country code where it originally occurred. 104 * <p> 105 * Note the country does not necessarily specifies the country of the phone number itself, but 106 * it is the country in which the user was in when the call was placed or received. 107 */ 108 private static final class NumberWithCountryIso { 109 public final String number; 110 public final String countryIso; 111 NumberWithCountryIso(String number, String countryIso)112 public NumberWithCountryIso(String number, String countryIso) { 113 this.number = number; 114 this.countryIso = countryIso; 115 } 116 117 @Override equals(Object o)118 public boolean equals(Object o) { 119 if (o == null) return false; 120 if (!(o instanceof NumberWithCountryIso)) return false; 121 NumberWithCountryIso other = (NumberWithCountryIso) o; 122 return TextUtils.equals(number, other.number) 123 && TextUtils.equals(countryIso, other.countryIso); 124 } 125 126 @Override hashCode()127 public int hashCode() { 128 return (number == null ? 0 : number.hashCode()) 129 ^ (countryIso == null ? 0 : countryIso.hashCode()); 130 } 131 } 132 133 /** The time in millis to delay starting the thread processing requests. */ 134 private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000; 135 136 /** The size of the cache of contact info. */ 137 private static final int CONTACT_INFO_CACHE_SIZE = 100; 138 139 /** Constant used to indicate no row is expanded. */ 140 private static final long NONE_EXPANDED = -1; 141 142 protected final Context mContext; 143 private final ContactInfoHelper mContactInfoHelper; 144 private final CallFetcher mCallFetcher; 145 private final Toast mReportedToast; 146 private final OnReportButtonClickListener mOnReportButtonClickListener; 147 private ViewTreeObserver mViewTreeObserver = null; 148 149 /** 150 * A cache of the contact details for the phone numbers in the call log. 151 * <p> 152 * The content of the cache is expired (but not purged) whenever the application comes to 153 * the foreground. 154 * <p> 155 * The key is number with the country in which the call was placed or received. 156 */ 157 private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache; 158 159 /** 160 * Tracks the call log row which was previously expanded. Used so that the closure of a 161 * previously expanded call log entry can be animated on rebind. 162 */ 163 private long mPreviouslyExpanded = NONE_EXPANDED; 164 165 /** 166 * Tracks the currently expanded call log row. 167 */ 168 private long mCurrentlyExpanded = NONE_EXPANDED; 169 170 /** 171 * Hashmap, keyed by call Id, used to track the day group for a call. As call log entries are 172 * put into the primary call groups in {@link com.android.dialer.calllog.CallLogGroupBuilder}, 173 * they are also assigned a secondary "day group". This hashmap tracks the day group assigned 174 * to all calls in the call log. This information is used to trigger the display of a day 175 * group header above the call log entry at the start of a day group. 176 * Note: Multiple calls are grouped into a single primary "call group" in the call log, and 177 * the cursor used to bind rows includes all of these calls. When determining if a day group 178 * change has occurred it is necessary to look at the last entry in the call log to determine 179 * its day group. This hashmap provides a means of determining the previous day group without 180 * having to reverse the cursor to the start of the previous day call log entry. 181 */ 182 private HashMap<Long,Integer> mDayGroups = new HashMap<Long, Integer>(); 183 184 /** 185 * A request for contact details for the given number. 186 */ 187 private static final class ContactInfoRequest { 188 /** The number to look-up. */ 189 public final String number; 190 /** The country in which a call to or from this number was placed or received. */ 191 public final String countryIso; 192 /** The cached contact information stored in the call log. */ 193 public final ContactInfo callLogInfo; 194 ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo)195 public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) { 196 this.number = number; 197 this.countryIso = countryIso; 198 this.callLogInfo = callLogInfo; 199 } 200 201 @Override equals(Object obj)202 public boolean equals(Object obj) { 203 if (this == obj) return true; 204 if (obj == null) return false; 205 if (!(obj instanceof ContactInfoRequest)) return false; 206 207 ContactInfoRequest other = (ContactInfoRequest) obj; 208 209 if (!TextUtils.equals(number, other.number)) return false; 210 if (!TextUtils.equals(countryIso, other.countryIso)) return false; 211 if (!Objects.equal(callLogInfo, other.callLogInfo)) return false; 212 213 return true; 214 } 215 216 @Override hashCode()217 public int hashCode() { 218 final int prime = 31; 219 int result = 1; 220 result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode()); 221 result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode()); 222 result = prime * result + ((number == null) ? 0 : number.hashCode()); 223 return result; 224 } 225 } 226 227 /** 228 * List of requests to update contact details. 229 * <p> 230 * Each request is made of a phone number to look up, and the contact info currently stored in 231 * the call log for this number. 232 * <p> 233 * The requests are added when displaying the contacts and are processed by a background 234 * thread. 235 */ 236 private final LinkedList<ContactInfoRequest> mRequests; 237 238 private boolean mLoading = true; 239 private static final int REDRAW = 1; 240 private static final int START_THREAD = 2; 241 242 private QueryThread mCallerIdThread; 243 244 /** Instance of helper class for managing views. */ 245 private final CallLogListItemHelper mCallLogViewsHelper; 246 247 /** Helper to set up contact photos. */ 248 private final ContactPhotoManager mContactPhotoManager; 249 /** Helper to parse and process phone numbers. */ 250 private PhoneNumberDisplayHelper mPhoneNumberHelper; 251 /** Helper to group call log entries. */ 252 private final CallLogGroupBuilder mCallLogGroupBuilder; 253 254 private CallItemExpandedListener mCallItemExpandedListener; 255 256 /** Can be set to true by tests to disable processing of requests. */ 257 private volatile boolean mRequestProcessingDisabled = false; 258 259 private boolean mIsCallLog = true; 260 261 private View mBadgeContainer; 262 private ImageView mBadgeImageView; 263 private TextView mBadgeText; 264 265 private int mCallLogBackgroundColor; 266 private int mExpandedBackgroundColor; 267 private float mExpandedTranslationZ; 268 269 /** Listener for the primary or secondary actions in the list. 270 * Primary opens the call details. 271 * Secondary calls or plays. 272 **/ 273 private final View.OnClickListener mActionListener = new View.OnClickListener() { 274 @Override 275 public void onClick(View view) { 276 startActivityForAction(view); 277 } 278 }; 279 280 /** 281 * The onClickListener used to expand or collapse the action buttons section for a call log 282 * entry. 283 */ 284 private final View.OnClickListener mExpandCollapseListener = new View.OnClickListener() { 285 @Override 286 public void onClick(View v) { 287 final CallLogListItemView callLogItem = (CallLogListItemView) v.getParent().getParent(); 288 handleRowExpanded(callLogItem, true /* animate */, false /* forceExpand */); 289 } 290 }; 291 292 private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() { 293 @Override 294 public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, 295 AccessibilityEvent event) { 296 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { 297 handleRowExpanded((CallLogListItemView) host, false /* animate */, 298 true /* forceExpand */); 299 } 300 return super.onRequestSendAccessibilityEvent(host, child, event); 301 } 302 }; 303 startActivityForAction(View view)304 private void startActivityForAction(View view) { 305 final IntentProvider intentProvider = (IntentProvider) view.getTag(); 306 if (intentProvider != null) { 307 final Intent intent = intentProvider.getIntent(mContext); 308 // See IntentProvider.getCallDetailIntentProvider() for why this may be null. 309 if (intent != null) { 310 DialerUtils.startActivityWithErrorToast(mContext, intent); 311 } 312 } 313 } 314 315 @Override onPreDraw()316 public boolean onPreDraw() { 317 // We only wanted to listen for the first draw (and this is it). 318 unregisterPreDrawListener(); 319 320 // Only schedule a thread-creation message if the thread hasn't been 321 // created yet. This is purely an optimization, to queue fewer messages. 322 if (mCallerIdThread == null) { 323 mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS); 324 } 325 326 return true; 327 } 328 329 private Handler mHandler = new Handler() { 330 @Override 331 public void handleMessage(Message msg) { 332 switch (msg.what) { 333 case REDRAW: 334 notifyDataSetChanged(); 335 break; 336 case START_THREAD: 337 startRequestProcessing(); 338 break; 339 } 340 } 341 }; 342 CallLogAdapter(Context context, CallFetcher callFetcher, ContactInfoHelper contactInfoHelper, CallItemExpandedListener callItemExpandedListener, OnReportButtonClickListener onReportButtonClickListener, boolean isCallLog)343 public CallLogAdapter(Context context, CallFetcher callFetcher, 344 ContactInfoHelper contactInfoHelper, CallItemExpandedListener callItemExpandedListener, 345 OnReportButtonClickListener onReportButtonClickListener, boolean isCallLog) { 346 super(context); 347 348 mContext = context; 349 mCallFetcher = callFetcher; 350 mContactInfoHelper = contactInfoHelper; 351 mIsCallLog = isCallLog; 352 mCallItemExpandedListener = callItemExpandedListener; 353 354 mOnReportButtonClickListener = onReportButtonClickListener; 355 mReportedToast = Toast.makeText(mContext, R.string.toast_caller_id_reported, 356 Toast.LENGTH_SHORT); 357 358 mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); 359 mRequests = new LinkedList<ContactInfoRequest>(); 360 361 Resources resources = mContext.getResources(); 362 CallTypeHelper callTypeHelper = new CallTypeHelper(resources); 363 mCallLogBackgroundColor = resources.getColor(R.color.background_dialer_list_items); 364 mExpandedBackgroundColor = resources.getColor(R.color.call_log_expanded_background_color); 365 mExpandedTranslationZ = resources.getDimension(R.dimen.call_log_expanded_translation_z); 366 367 mContactPhotoManager = ContactPhotoManager.getInstance(mContext); 368 mPhoneNumberHelper = new PhoneNumberDisplayHelper(resources); 369 PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper( 370 resources, callTypeHelper, new PhoneNumberUtilsWrapper()); 371 mCallLogViewsHelper = 372 new CallLogListItemHelper( 373 phoneCallDetailsHelper, mPhoneNumberHelper, resources); 374 mCallLogGroupBuilder = new CallLogGroupBuilder(this); 375 } 376 377 /** 378 * Requery on background thread when {@link Cursor} changes. 379 */ 380 @Override onContentChanged()381 protected void onContentChanged() { 382 mCallFetcher.fetchCalls(); 383 } 384 setLoading(boolean loading)385 public void setLoading(boolean loading) { 386 mLoading = loading; 387 } 388 389 @Override isEmpty()390 public boolean isEmpty() { 391 if (mLoading) { 392 // We don't want the empty state to show when loading. 393 return false; 394 } else { 395 return super.isEmpty(); 396 } 397 } 398 399 /** 400 * Starts a background thread to process contact-lookup requests, unless one 401 * has already been started. 402 */ startRequestProcessing()403 private synchronized void startRequestProcessing() { 404 // For unit-testing. 405 if (mRequestProcessingDisabled) return; 406 407 // Idempotence... if a thread is already started, don't start another. 408 if (mCallerIdThread != null) return; 409 410 mCallerIdThread = new QueryThread(); 411 mCallerIdThread.setPriority(Thread.MIN_PRIORITY); 412 mCallerIdThread.start(); 413 } 414 415 /** 416 * Stops the background thread that processes updates and cancels any 417 * pending requests to start it. 418 */ stopRequestProcessing()419 public synchronized void stopRequestProcessing() { 420 // Remove any pending requests to start the processing thread. 421 mHandler.removeMessages(START_THREAD); 422 if (mCallerIdThread != null) { 423 // Stop the thread; we are finished with it. 424 mCallerIdThread.stopProcessing(); 425 mCallerIdThread.interrupt(); 426 mCallerIdThread = null; 427 } 428 } 429 430 /** 431 * Stop receiving onPreDraw() notifications. 432 */ unregisterPreDrawListener()433 private void unregisterPreDrawListener() { 434 if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) { 435 mViewTreeObserver.removeOnPreDrawListener(this); 436 } 437 mViewTreeObserver = null; 438 } 439 invalidateCache()440 public void invalidateCache() { 441 mContactInfoCache.expireAll(); 442 443 // Restart the request-processing thread after the next draw. 444 stopRequestProcessing(); 445 unregisterPreDrawListener(); 446 } 447 448 /** 449 * Enqueues a request to look up the contact details for the given phone number. 450 * <p> 451 * It also provides the current contact info stored in the call log for this number. 452 * <p> 453 * If the {@code immediate} parameter is true, it will start immediately the thread that looks 454 * up the contact information (if it has not been already started). Otherwise, it will be 455 * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}. 456 */ enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, boolean immediate)457 protected void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, 458 boolean immediate) { 459 ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo); 460 synchronized (mRequests) { 461 if (!mRequests.contains(request)) { 462 mRequests.add(request); 463 mRequests.notifyAll(); 464 } 465 } 466 if (immediate) startRequestProcessing(); 467 } 468 469 /** 470 * Queries the appropriate content provider for the contact associated with the number. 471 * <p> 472 * Upon completion it also updates the cache in the call log, if it is different from 473 * {@code callLogInfo}. 474 * <p> 475 * The number might be either a SIP address or a phone number. 476 * <p> 477 * It returns true if it updated the content of the cache and we should therefore tell the 478 * view to update its content. 479 */ queryContactInfo(String number, String countryIso, ContactInfo callLogInfo)480 private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) { 481 final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso); 482 483 if (info == null) { 484 // The lookup failed, just return without requesting to update the view. 485 return false; 486 } 487 488 // Check the existing entry in the cache: only if it has changed we should update the 489 // view. 490 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 491 ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso); 492 493 final boolean isRemoteSource = info.sourceType != 0; 494 495 // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY} 496 // to avoid updating the data set for every new row that is scrolled into view. 497 // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/) 498 499 // Exception: Photo uris for contacts from remote sources are not cached in the call log 500 // cache, so we have to force a redraw for these contacts regardless. 501 boolean updated = (existingInfo != ContactInfo.EMPTY || isRemoteSource) && 502 !info.equals(existingInfo); 503 504 // Store the data in the cache so that the UI thread can use to display it. Store it 505 // even if it has not changed so that it is marked as not expired. 506 mContactInfoCache.put(numberCountryIso, info); 507 // Update the call log even if the cache it is up-to-date: it is possible that the cache 508 // contains the value from a different call log entry. 509 updateCallLogContactInfoCache(number, countryIso, info, callLogInfo); 510 return updated; 511 } 512 513 /* 514 * Handles requests for contact name and number type. 515 */ 516 private class QueryThread extends Thread { 517 private volatile boolean mDone = false; 518 QueryThread()519 public QueryThread() { 520 super("CallLogAdapter.QueryThread"); 521 } 522 stopProcessing()523 public void stopProcessing() { 524 mDone = true; 525 } 526 527 @Override run()528 public void run() { 529 boolean needRedraw = false; 530 while (true) { 531 // Check if thread is finished, and if so return immediately. 532 if (mDone) return; 533 534 // Obtain next request, if any is available. 535 // Keep synchronized section small. 536 ContactInfoRequest req = null; 537 synchronized (mRequests) { 538 if (!mRequests.isEmpty()) { 539 req = mRequests.removeFirst(); 540 } 541 } 542 543 if (req != null) { 544 // Process the request. If the lookup succeeds, schedule a 545 // redraw. 546 needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo); 547 } else { 548 // Throttle redraw rate by only sending them when there are 549 // more requests. 550 if (needRedraw) { 551 needRedraw = false; 552 mHandler.sendEmptyMessage(REDRAW); 553 } 554 555 // Wait until another request is available, or until this 556 // thread is no longer needed (as indicated by being 557 // interrupted). 558 try { 559 synchronized (mRequests) { 560 mRequests.wait(1000); 561 } 562 } catch (InterruptedException ie) { 563 // Ignore, and attempt to continue processing requests. 564 } 565 } 566 } 567 } 568 } 569 570 @Override addGroups(Cursor cursor)571 protected void addGroups(Cursor cursor) { 572 mCallLogGroupBuilder.addGroups(cursor); 573 } 574 575 @Override newStandAloneView(Context context, ViewGroup parent)576 protected View newStandAloneView(Context context, ViewGroup parent) { 577 return newChildView(context, parent); 578 } 579 580 @Override newGroupView(Context context, ViewGroup parent)581 protected View newGroupView(Context context, ViewGroup parent) { 582 return newChildView(context, parent); 583 } 584 585 @Override newChildView(Context context, ViewGroup parent)586 protected View newChildView(Context context, ViewGroup parent) { 587 LayoutInflater inflater = LayoutInflater.from(context); 588 CallLogListItemView view = 589 (CallLogListItemView) inflater.inflate(R.layout.call_log_list_item, parent, false); 590 591 // Get the views to bind to and cache them. 592 CallLogListItemViews views = CallLogListItemViews.fromView(view); 593 view.setTag(views); 594 595 // Set text height to false on the TextViews so they don't have extra padding. 596 views.phoneCallDetailsViews.nameView.setElegantTextHeight(false); 597 views.phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false); 598 599 return view; 600 } 601 602 @Override bindStandAloneView(View view, Context context, Cursor cursor)603 protected void bindStandAloneView(View view, Context context, Cursor cursor) { 604 bindView(view, cursor, 1); 605 } 606 607 @Override bindChildView(View view, Context context, Cursor cursor)608 protected void bindChildView(View view, Context context, Cursor cursor) { 609 bindView(view, cursor, 1); 610 } 611 612 @Override bindGroupView(View view, Context context, Cursor cursor, int groupSize, boolean expanded)613 protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize, 614 boolean expanded) { 615 bindView(view, cursor, groupSize); 616 } 617 findAndCacheViews(View view)618 private void findAndCacheViews(View view) { 619 } 620 621 /** 622 * Binds the views in the entry to the data in the call log. 623 * 624 * @param view the view corresponding to this entry 625 * @param c the cursor pointing to the entry in the call log 626 * @param count the number of entries in the current item, greater than 1 if it is a group 627 */ bindView(View view, Cursor c, int count)628 private void bindView(View view, Cursor c, int count) { 629 view.setAccessibilityDelegate(mAccessibilityDelegate); 630 final CallLogListItemView callLogItemView = (CallLogListItemView) view; 631 final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); 632 633 // Default case: an item in the call log. 634 views.primaryActionView.setVisibility(View.VISIBLE); 635 636 final String number = c.getString(CallLogQuery.NUMBER); 637 final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION); 638 final long date = c.getLong(CallLogQuery.DATE); 639 final long duration = c.getLong(CallLogQuery.DURATION); 640 final int callType = c.getInt(CallLogQuery.CALL_TYPE); 641 final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount( 642 c.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME), 643 c.getString(CallLogQuery.ACCOUNT_ID)); 644 final Drawable accountIcon = PhoneAccountUtils.getAccountIcon(mContext, 645 accountHandle); 646 final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO); 647 648 final long rowId = c.getLong(CallLogQuery.ID); 649 views.rowId = rowId; 650 651 // For entries in the call log, check if the day group has changed and display a header 652 // if necessary. 653 if (mIsCallLog) { 654 int currentGroup = getDayGroupForCall(rowId); 655 int previousGroup = getPreviousDayGroup(c); 656 if (currentGroup != previousGroup) { 657 views.dayGroupHeader.setVisibility(View.VISIBLE); 658 views.dayGroupHeader.setText(getGroupDescription(currentGroup)); 659 } else { 660 views.dayGroupHeader.setVisibility(View.GONE); 661 } 662 } else { 663 views.dayGroupHeader.setVisibility(View.GONE); 664 } 665 666 // Store some values used when the actions ViewStub is inflated on expansion of the actions 667 // section. 668 views.number = number; 669 views.numberPresentation = numberPresentation; 670 views.callType = callType; 671 // NOTE: This is currently not being used, but can be used in future versions. 672 views.accountHandle = accountHandle; 673 views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); 674 // Stash away the Ids of the calls so that we can support deleting a row in the call log. 675 views.callIds = getCallIds(c, count); 676 677 final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c); 678 679 final boolean isVoicemailNumber = 680 PhoneNumberUtilsWrapper.INSTANCE.isVoicemailNumber(number); 681 682 // Where binding and not in the call log, use default behaviour of invoking a call when 683 // tapping the primary view. 684 if (!mIsCallLog) { 685 views.primaryActionView.setOnClickListener(this.mActionListener); 686 687 // Set return call intent, otherwise null. 688 if (PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) { 689 // Sets the primary action to call the number. 690 views.primaryActionView.setTag(IntentProvider.getReturnCallIntentProvider(number)); 691 } else { 692 // Number is not callable, so hide button. 693 views.primaryActionView.setTag(null); 694 } 695 } else { 696 // In the call log, expand/collapse an actions section for the call log entry when 697 // the primary view is tapped. 698 views.primaryActionView.setOnClickListener(this.mExpandCollapseListener); 699 700 // Note: Binding of the action buttons is done as required in configureActionViews 701 // when the user expands the actions ViewStub. 702 } 703 704 // Lookup contacts with this number 705 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 706 ExpirableCache.CachedValue<ContactInfo> cachedInfo = 707 mContactInfoCache.getCachedValue(numberCountryIso); 708 ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); 709 if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation) 710 || isVoicemailNumber) { 711 // If this is a number that cannot be dialed, there is no point in looking up a contact 712 // for it. 713 info = ContactInfo.EMPTY; 714 } else if (cachedInfo == null) { 715 mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY); 716 // Use the cached contact info from the call log. 717 info = cachedContactInfo; 718 // The db request should happen on a non-UI thread. 719 // Request the contact details immediately since they are currently missing. 720 enqueueRequest(number, countryIso, cachedContactInfo, true); 721 // We will format the phone number when we make the background request. 722 } else { 723 if (cachedInfo.isExpired()) { 724 // The contact info is no longer up to date, we should request it. However, we 725 // do not need to request them immediately. 726 enqueueRequest(number, countryIso, cachedContactInfo, false); 727 } else if (!callLogInfoMatches(cachedContactInfo, info)) { 728 // The call log information does not match the one we have, look it up again. 729 // We could simply update the call log directly, but that needs to be done in a 730 // background thread, so it is easier to simply request a new lookup, which will, as 731 // a side-effect, update the call log. 732 enqueueRequest(number, countryIso, cachedContactInfo, false); 733 } 734 735 if (info == ContactInfo.EMPTY) { 736 // Use the cached contact info from the call log. 737 info = cachedContactInfo; 738 } 739 } 740 741 final Uri lookupUri = info.lookupUri; 742 final String name = info.name; 743 final int ntype = info.type; 744 final String label = info.label; 745 final long photoId = info.photoId; 746 final Uri photoUri = info.photoUri; 747 CharSequence formattedNumber = info.formattedNumber; 748 final int[] callTypes = getCallTypes(c, count); 749 final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION); 750 final int sourceType = info.sourceType; 751 final int features = getCallFeatures(c, count); 752 final String transcription = c.getString(CallLogQuery.TRANSCRIPTION); 753 Long dataUsage = null; 754 if (!c.isNull(CallLogQuery.DATA_USAGE)) { 755 dataUsage = c.getLong(CallLogQuery.DATA_USAGE); 756 } 757 758 final PhoneCallDetails details; 759 760 views.reported = info.isBadData; 761 762 // The entry can only be reported as invalid if it has a valid ID and the source of the 763 // entry supports marking entries as invalid. 764 views.canBeReportedAsInvalid = mContactInfoHelper.canReportAsInvalid(info.sourceType, 765 info.objectId); 766 767 // Restore expansion state of the row on rebind. Inflate the actions ViewStub if required, 768 // and set its visibility state accordingly. 769 expandOrCollapseActions(callLogItemView, isExpanded(rowId)); 770 771 if (TextUtils.isEmpty(name)) { 772 details = new PhoneCallDetails(number, numberPresentation, 773 formattedNumber, countryIso, geocode, callTypes, date, 774 duration, null, accountIcon, features, dataUsage, transcription); 775 } else { 776 details = new PhoneCallDetails(number, numberPresentation, 777 formattedNumber, countryIso, geocode, callTypes, date, 778 duration, name, ntype, label, lookupUri, photoUri, sourceType, 779 null, accountIcon, features, dataUsage, transcription); 780 } 781 782 mCallLogViewsHelper.setPhoneCallDetails(mContext, views, details); 783 784 int contactType = ContactPhotoManager.TYPE_DEFAULT; 785 786 if (isVoicemailNumber) { 787 contactType = ContactPhotoManager.TYPE_VOICEMAIL; 788 } else if (mContactInfoHelper.isBusiness(info.sourceType)) { 789 contactType = ContactPhotoManager.TYPE_BUSINESS; 790 } 791 792 String lookupKey = lookupUri == null ? null 793 : ContactInfoHelper.getLookupKeyFromUri(lookupUri); 794 795 String nameForDefaultImage = null; 796 if (TextUtils.isEmpty(name)) { 797 nameForDefaultImage = mPhoneNumberHelper.getDisplayNumber(details.number, 798 details.numberPresentation, details.formattedNumber).toString(); 799 } else { 800 nameForDefaultImage = name; 801 } 802 803 if (photoId == 0 && photoUri != null) { 804 setPhoto(views, photoUri, lookupUri, nameForDefaultImage, lookupKey, contactType); 805 } else { 806 setPhoto(views, photoId, lookupUri, nameForDefaultImage, lookupKey, contactType); 807 } 808 809 // Listen for the first draw 810 if (mViewTreeObserver == null) { 811 mViewTreeObserver = view.getViewTreeObserver(); 812 mViewTreeObserver.addOnPreDrawListener(this); 813 } 814 815 bindBadge(view, info, details, callType); 816 } 817 818 /** 819 * Retrieves the day group of the previous call in the call log. Used to determine if the day 820 * group has changed and to trigger display of the day group text. 821 * 822 * @param cursor The call log cursor. 823 * @return The previous day group, or DAY_GROUP_NONE if this is the first call. 824 */ getPreviousDayGroup(Cursor cursor)825 private int getPreviousDayGroup(Cursor cursor) { 826 // We want to restore the position in the cursor at the end. 827 int startingPosition = cursor.getPosition(); 828 int dayGroup = CallLogGroupBuilder.DAY_GROUP_NONE; 829 if (cursor.moveToPrevious()) { 830 long previousRowId = cursor.getLong(CallLogQuery.ID); 831 dayGroup = getDayGroupForCall(previousRowId); 832 } 833 cursor.moveToPosition(startingPosition); 834 return dayGroup; 835 } 836 837 /** 838 * Given a call Id, look up the day group that the call belongs to. The day group data is 839 * populated in {@link com.android.dialer.calllog.CallLogGroupBuilder}. 840 * 841 * @param callId The call to retrieve the day group for. 842 * @return The day group for the call. 843 */ getDayGroupForCall(long callId)844 private int getDayGroupForCall(long callId) { 845 if (mDayGroups.containsKey(callId)) { 846 return mDayGroups.get(callId); 847 } 848 return CallLogGroupBuilder.DAY_GROUP_NONE; 849 } 850 /** 851 * Determines if a call log row with the given Id is expanded. 852 * @param rowId The row Id of the call. 853 * @return True if the row should be expanded. 854 */ isExpanded(long rowId)855 private boolean isExpanded(long rowId) { 856 return mCurrentlyExpanded == rowId; 857 } 858 859 /** 860 * Toggles the expansion state tracked for the call log row identified by rowId and returns 861 * the new expansion state. Assumes that only a single call log row will be expanded at any 862 * one point and tracks the current and previous expanded item. 863 * 864 * @param rowId The row Id associated with the call log row to expand/collapse. 865 * @return True where the row is now expanded, false otherwise. 866 */ toggleExpansion(long rowId)867 private boolean toggleExpansion(long rowId) { 868 if (rowId == mCurrentlyExpanded) { 869 // Collapsing currently expanded row. 870 mPreviouslyExpanded = NONE_EXPANDED; 871 mCurrentlyExpanded = NONE_EXPANDED; 872 873 return false; 874 } else { 875 // Expanding a row (collapsing current expanded one). 876 877 mPreviouslyExpanded = mCurrentlyExpanded; 878 mCurrentlyExpanded = rowId; 879 return true; 880 } 881 } 882 883 /** 884 * Expands or collapses the view containing the CALLBACK, VOICEMAIL and DETAILS action buttons. 885 * 886 * @param callLogItem The call log entry parent view. 887 * @param isExpanded The new expansion state of the view. 888 */ expandOrCollapseActions(CallLogListItemView callLogItem, boolean isExpanded)889 private void expandOrCollapseActions(CallLogListItemView callLogItem, boolean isExpanded) { 890 final CallLogListItemViews views = (CallLogListItemViews)callLogItem.getTag(); 891 892 expandVoicemailTranscriptionView(views, isExpanded); 893 if (isExpanded) { 894 // Inflate the view stub if necessary, and wire up the event handlers. 895 inflateActionViewStub(callLogItem); 896 897 views.actionsView.setVisibility(View.VISIBLE); 898 views.actionsView.setAlpha(1.0f); 899 views.callLogEntryView.setBackgroundColor(mExpandedBackgroundColor); 900 views.callLogEntryView.setTranslationZ(mExpandedTranslationZ); 901 callLogItem.setTranslationZ(mExpandedTranslationZ); // WAR 902 } else { 903 // When recycling a view, it is possible the actionsView ViewStub was previously 904 // inflated so we should hide it in this case. 905 if (views.actionsView != null) { 906 views.actionsView.setVisibility(View.GONE); 907 } 908 909 views.callLogEntryView.setBackgroundColor(mCallLogBackgroundColor); 910 views.callLogEntryView.setTranslationZ(0); 911 callLogItem.setTranslationZ(0); // WAR 912 } 913 } 914 expandVoicemailTranscriptionView(CallLogListItemViews views, boolean isExpanded)915 public static void expandVoicemailTranscriptionView(CallLogListItemViews views, 916 boolean isExpanded) { 917 if (views.callType != Calls.VOICEMAIL_TYPE) { 918 return; 919 } 920 921 final TextView view = views.phoneCallDetailsViews.voicemailTranscriptionView; 922 if (TextUtils.isEmpty(view.getText())) { 923 return; 924 } 925 view.setMaxLines(isExpanded ? VOICEMAIL_TRANSCRIPTION_MAX_LINES : 1); 926 view.setSingleLine(!isExpanded); 927 } 928 929 /** 930 * Configures the action buttons in the expandable actions ViewStub. The ViewStub is not 931 * inflated during initial binding, so click handlers, tags and accessibility text must be set 932 * here, if necessary. 933 * 934 * @param callLogItem The call log list item view. 935 */ inflateActionViewStub(final View callLogItem)936 private void inflateActionViewStub(final View callLogItem) { 937 final CallLogListItemViews views = (CallLogListItemViews)callLogItem.getTag(); 938 939 ViewStub stub = (ViewStub)callLogItem.findViewById(R.id.call_log_entry_actions_stub); 940 if (stub != null) { 941 views.actionsView = (ViewGroup) stub.inflate(); 942 } 943 944 if (views.callBackButtonView == null) { 945 views.callBackButtonView = (TextView)views.actionsView.findViewById( 946 R.id.call_back_action); 947 } 948 949 if (views.videoCallButtonView == null) { 950 views.videoCallButtonView = (TextView)views.actionsView.findViewById( 951 R.id.video_call_action); 952 } 953 954 if (views.voicemailButtonView == null) { 955 views.voicemailButtonView = (TextView)views.actionsView.findViewById( 956 R.id.voicemail_action); 957 } 958 959 if (views.detailsButtonView == null) { 960 views.detailsButtonView = (TextView)views.actionsView.findViewById(R.id.details_action); 961 } 962 963 if (views.reportButtonView == null) { 964 views.reportButtonView = (TextView)views.actionsView.findViewById(R.id.report_action); 965 views.reportButtonView.setOnClickListener(new View.OnClickListener() { 966 @Override 967 public void onClick(View v) { 968 if (mOnReportButtonClickListener != null) { 969 mOnReportButtonClickListener.onReportButtonClick(views.number); 970 } 971 } 972 }); 973 } 974 975 bindActionButtons(views); 976 } 977 978 /*** 979 * Binds click handlers and intents to the voicemail, details and callback action buttons. 980 * 981 * @param views The call log item views. 982 */ bindActionButtons(CallLogListItemViews views)983 private void bindActionButtons(CallLogListItemViews views) { 984 boolean canPlaceCallToNumber = 985 PhoneNumberUtilsWrapper.canPlaceCallsTo(views.number, views.numberPresentation); 986 // Set return call intent, otherwise null. 987 if (canPlaceCallToNumber) { 988 // Sets the primary action to call the number. 989 views.callBackButtonView.setTag( 990 IntentProvider.getReturnCallIntentProvider(views.number)); 991 views.callBackButtonView.setVisibility(View.VISIBLE); 992 views.callBackButtonView.setOnClickListener(mActionListener); 993 } else { 994 // Number is not callable, so hide button. 995 views.callBackButtonView.setTag(null); 996 views.callBackButtonView.setVisibility(View.GONE); 997 } 998 999 // If one of the calls had video capabilities, show the video call button. 1000 if (CallUtil.isVideoEnabled(mContext) && canPlaceCallToNumber && 1001 views.phoneCallDetailsViews.callTypeIcons.isVideoShown()) { 1002 views.videoCallButtonView.setTag( 1003 IntentProvider.getReturnVideoCallIntentProvider(views.number)); 1004 views.videoCallButtonView.setVisibility(View.VISIBLE); 1005 views.videoCallButtonView.setOnClickListener(mActionListener); 1006 } else { 1007 views.videoCallButtonView.setTag(null); 1008 views.videoCallButtonView.setVisibility(View.GONE); 1009 } 1010 1011 // For voicemail calls, show the "VOICEMAIL" action button; hide otherwise. 1012 if (views.callType == Calls.VOICEMAIL_TYPE) { 1013 views.voicemailButtonView.setOnClickListener(mActionListener); 1014 views.voicemailButtonView.setTag( 1015 IntentProvider.getPlayVoicemailIntentProvider( 1016 views.rowId, views.voicemailUri)); 1017 views.voicemailButtonView.setVisibility(View.VISIBLE); 1018 1019 views.detailsButtonView.setVisibility(View.GONE); 1020 } else { 1021 views.voicemailButtonView.setTag(null); 1022 views.voicemailButtonView.setVisibility(View.GONE); 1023 1024 views.detailsButtonView.setOnClickListener(mActionListener); 1025 views.detailsButtonView.setTag( 1026 IntentProvider.getCallDetailIntentProvider( 1027 views.rowId, views.callIds, null) 1028 ); 1029 1030 if (views.canBeReportedAsInvalid && !views.reported) { 1031 views.reportButtonView.setVisibility(View.VISIBLE); 1032 } else { 1033 views.reportButtonView.setVisibility(View.GONE); 1034 } 1035 } 1036 1037 mCallLogViewsHelper.setActionContentDescriptions(views); 1038 } 1039 bindBadge( View view, ContactInfo info, final PhoneCallDetails details, int callType)1040 protected void bindBadge( 1041 View view, ContactInfo info, final PhoneCallDetails details, int callType) { 1042 // Do not show badge in call log. 1043 if (!mIsCallLog) { 1044 final ViewStub stub = (ViewStub) view.findViewById(R.id.link_stub); 1045 if (UriUtils.isEncodedContactUri(info.lookupUri)) { 1046 if (stub != null) { 1047 final View inflated = stub.inflate(); 1048 inflated.setVisibility(View.VISIBLE); 1049 mBadgeContainer = inflated.findViewById(R.id.badge_link_container); 1050 mBadgeImageView = (ImageView) inflated.findViewById(R.id.badge_image); 1051 mBadgeText = (TextView) inflated.findViewById(R.id.badge_text); 1052 } 1053 1054 mBadgeContainer.setOnClickListener(new View.OnClickListener() { 1055 @Override 1056 public void onClick(View v) { 1057 final Intent intent = 1058 DialtactsActivity.getAddNumberToContactIntent(details.number); 1059 mContext.startActivity(intent); 1060 } 1061 }); 1062 mBadgeImageView.setImageResource(R.drawable.ic_person_add_24dp); 1063 mBadgeText.setText(R.string.recentCalls_addToContact); 1064 } else { 1065 // Hide badge if it was previously shown. 1066 if (stub == null) { 1067 final View container = view.findViewById(R.id.badge_container); 1068 if (container != null) { 1069 container.setVisibility(View.GONE); 1070 } 1071 } 1072 } 1073 } 1074 } 1075 1076 /** Checks whether the contact info from the call log matches the one from the contacts db. */ callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info)1077 private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) { 1078 // The call log only contains a subset of the fields in the contacts db. 1079 // Only check those. 1080 return TextUtils.equals(callLogInfo.name, info.name) 1081 && callLogInfo.type == info.type 1082 && TextUtils.equals(callLogInfo.label, info.label); 1083 } 1084 1085 /** 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)1086 private void updateCallLogContactInfoCache(String number, String countryIso, 1087 ContactInfo updatedInfo, ContactInfo callLogInfo) { 1088 final ContentValues values = new ContentValues(); 1089 boolean needsUpdate = false; 1090 1091 if (callLogInfo != null) { 1092 if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) { 1093 values.put(Calls.CACHED_NAME, updatedInfo.name); 1094 needsUpdate = true; 1095 } 1096 1097 if (updatedInfo.type != callLogInfo.type) { 1098 values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); 1099 needsUpdate = true; 1100 } 1101 1102 if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) { 1103 values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); 1104 needsUpdate = true; 1105 } 1106 if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) { 1107 values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); 1108 needsUpdate = true; 1109 } 1110 // Only replace the normalized number if the new updated normalized number isn't empty. 1111 if (!TextUtils.isEmpty(updatedInfo.normalizedNumber) && 1112 !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) { 1113 values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); 1114 needsUpdate = true; 1115 } 1116 if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) { 1117 values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); 1118 needsUpdate = true; 1119 } 1120 if (updatedInfo.photoId != callLogInfo.photoId) { 1121 values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); 1122 needsUpdate = true; 1123 } 1124 if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) { 1125 values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); 1126 needsUpdate = true; 1127 } 1128 } else { 1129 // No previous values, store all of them. 1130 values.put(Calls.CACHED_NAME, updatedInfo.name); 1131 values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); 1132 values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); 1133 values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); 1134 values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); 1135 values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); 1136 values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); 1137 values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); 1138 needsUpdate = true; 1139 } 1140 1141 if (!needsUpdate) return; 1142 1143 if (countryIso == null) { 1144 mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values, 1145 Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL", 1146 new String[]{ number }); 1147 } else { 1148 mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values, 1149 Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?", 1150 new String[]{ number, countryIso }); 1151 } 1152 } 1153 1154 /** Returns the contact information as stored in the call log. */ getContactInfoFromCallLog(Cursor c)1155 private ContactInfo getContactInfoFromCallLog(Cursor c) { 1156 ContactInfo info = new ContactInfo(); 1157 info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI)); 1158 info.name = c.getString(CallLogQuery.CACHED_NAME); 1159 info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE); 1160 info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL); 1161 String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER); 1162 info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber; 1163 info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER); 1164 info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID); 1165 info.photoUri = null; // We do not cache the photo URI. 1166 info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER); 1167 return info; 1168 } 1169 1170 /** 1171 * Returns the call types for the given number of items in the cursor. 1172 * <p> 1173 * It uses the next {@code count} rows in the cursor to extract the types. 1174 * <p> 1175 * It position in the cursor is unchanged by this function. 1176 */ getCallTypes(Cursor cursor, int count)1177 private int[] getCallTypes(Cursor cursor, int count) { 1178 int position = cursor.getPosition(); 1179 int[] callTypes = new int[count]; 1180 for (int index = 0; index < count; ++index) { 1181 callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); 1182 cursor.moveToNext(); 1183 } 1184 cursor.moveToPosition(position); 1185 return callTypes; 1186 } 1187 1188 /** 1189 * Determine the features which were enabled for any of the calls that make up a call log 1190 * entry. 1191 * 1192 * @param cursor The cursor. 1193 * @param count The number of calls for the current call log entry. 1194 * @return The features. 1195 */ getCallFeatures(Cursor cursor, int count)1196 private int getCallFeatures(Cursor cursor, int count) { 1197 int features = 0; 1198 int position = cursor.getPosition(); 1199 for (int index = 0; index < count; ++index) { 1200 features |= cursor.getInt(CallLogQuery.FEATURES); 1201 cursor.moveToNext(); 1202 } 1203 cursor.moveToPosition(position); 1204 return features; 1205 } 1206 setPhoto(CallLogListItemViews views, long photoId, Uri contactUri, String displayName, String identifier, int contactType)1207 private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri, 1208 String displayName, String identifier, int contactType) { 1209 views.quickContactView.assignContactUri(contactUri); 1210 views.quickContactView.setOverlay(null); 1211 DefaultImageRequest request = new DefaultImageRequest(displayName, identifier, 1212 contactType, true /* isCircular */); 1213 mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, false /* darkTheme */, 1214 true /* isCircular */, request); 1215 } 1216 setPhoto(CallLogListItemViews views, Uri photoUri, Uri contactUri, String displayName, String identifier, int contactType)1217 private void setPhoto(CallLogListItemViews views, Uri photoUri, Uri contactUri, 1218 String displayName, String identifier, int contactType) { 1219 views.quickContactView.assignContactUri(contactUri); 1220 views.quickContactView.setOverlay(null); 1221 DefaultImageRequest request = new DefaultImageRequest(displayName, identifier, 1222 contactType, true /* isCircular */); 1223 mContactPhotoManager.loadDirectoryPhoto(views.quickContactView, photoUri, 1224 false /* darkTheme */, true /* isCircular */, request); 1225 } 1226 1227 /** 1228 * Bind a call log entry view for testing purposes. Also inflates the action view stub so 1229 * unit tests can access the buttons contained within. 1230 * 1231 * @param view The current call log row. 1232 * @param context The current context. 1233 * @param cursor The cursor to bind from. 1234 */ 1235 @VisibleForTesting bindViewForTest(View view, Context context, Cursor cursor)1236 void bindViewForTest(View view, Context context, Cursor cursor) { 1237 bindStandAloneView(view, context, cursor); 1238 inflateActionViewStub(view); 1239 } 1240 1241 /** 1242 * Sets whether processing of requests for contact details should be enabled. 1243 * <p> 1244 * This method should be called in tests to disable such processing of requests when not 1245 * needed. 1246 */ 1247 @VisibleForTesting disableRequestProcessingForTest()1248 void disableRequestProcessingForTest() { 1249 mRequestProcessingDisabled = true; 1250 } 1251 1252 @VisibleForTesting injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo)1253 void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { 1254 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 1255 mContactInfoCache.put(numberCountryIso, contactInfo); 1256 } 1257 1258 @Override addGroup(int cursorPosition, int size, boolean expanded)1259 public void addGroup(int cursorPosition, int size, boolean expanded) { 1260 super.addGroup(cursorPosition, size, expanded); 1261 } 1262 1263 /** 1264 * Stores the day group associated with a call in the call log. 1265 * 1266 * @param rowId The row Id of the current call. 1267 * @param dayGroup The day group the call belongs in. 1268 */ 1269 @Override setDayGroup(long rowId, int dayGroup)1270 public void setDayGroup(long rowId, int dayGroup) { 1271 if (!mDayGroups.containsKey(rowId)) { 1272 mDayGroups.put(rowId, dayGroup); 1273 } 1274 } 1275 1276 /** 1277 * Clears the day group associations on re-bind of the call log. 1278 */ 1279 @Override clearDayGroups()1280 public void clearDayGroups() { 1281 mDayGroups.clear(); 1282 } 1283 1284 /* 1285 * Get the number from the Contacts, if available, since sometimes 1286 * the number provided by caller id may not be formatted properly 1287 * depending on the carrier (roaming) in use at the time of the 1288 * incoming call. 1289 * Logic : If the caller-id number starts with a "+", use it 1290 * Else if the number in the contacts starts with a "+", use that one 1291 * Else if the number in the contacts is longer, use that one 1292 */ getBetterNumberFromContacts(String number, String countryIso)1293 public String getBetterNumberFromContacts(String number, String countryIso) { 1294 String matchingNumber = null; 1295 // Look in the cache first. If it's not found then query the Phones db 1296 NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); 1297 ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso); 1298 if (ci != null && ci != ContactInfo.EMPTY) { 1299 matchingNumber = ci.number; 1300 } else { 1301 try { 1302 Cursor phonesCursor = mContext.getContentResolver().query( 1303 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number), 1304 PhoneQuery._PROJECTION, null, null, null); 1305 if (phonesCursor != null) { 1306 try { 1307 if (phonesCursor.moveToFirst()) { 1308 matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER); 1309 } 1310 } finally { 1311 phonesCursor.close(); 1312 } 1313 } 1314 } catch (Exception e) { 1315 // Use the number from the call log 1316 } 1317 } 1318 if (!TextUtils.isEmpty(matchingNumber) && 1319 (matchingNumber.startsWith("+") 1320 || matchingNumber.length() > number.length())) { 1321 number = matchingNumber; 1322 } 1323 return number; 1324 } 1325 1326 /** 1327 * Retrieves the call Ids represented by the current call log row. 1328 * 1329 * @param cursor Call log cursor to retrieve call Ids from. 1330 * @param groupSize Number of calls associated with the current call log row. 1331 * @return Array of call Ids. 1332 */ getCallIds(final Cursor cursor, final int groupSize)1333 private long[] getCallIds(final Cursor cursor, final int groupSize) { 1334 // We want to restore the position in the cursor at the end. 1335 int startingPosition = cursor.getPosition(); 1336 long[] ids = new long[groupSize]; 1337 // Copy the ids of the rows in the group. 1338 for (int index = 0; index < groupSize; ++index) { 1339 ids[index] = cursor.getLong(CallLogQuery.ID); 1340 cursor.moveToNext(); 1341 } 1342 cursor.moveToPosition(startingPosition); 1343 return ids; 1344 } 1345 1346 /** 1347 * Determines the description for a day group. 1348 * 1349 * @param group The day group to retrieve the description for. 1350 * @return The day group description. 1351 */ getGroupDescription(int group)1352 private CharSequence getGroupDescription(int group) { 1353 if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) { 1354 return mContext.getResources().getString(R.string.call_log_header_today); 1355 } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) { 1356 return mContext.getResources().getString(R.string.call_log_header_yesterday); 1357 } else { 1358 return mContext.getResources().getString(R.string.call_log_header_other); 1359 } 1360 } 1361 onBadDataReported(String number)1362 public void onBadDataReported(String number) { 1363 mContactInfoCache.expireAll(); 1364 mReportedToast.show(); 1365 } 1366 1367 /** 1368 * Manages the state changes for the UI interaction where a call log row is expanded. 1369 * 1370 * @param view The view that was tapped 1371 * @param animate Whether or not to animate the expansion/collapse 1372 * @param forceExpand Whether or not to force the call log row into an expanded state regardless 1373 * of its previous state 1374 */ handleRowExpanded(CallLogListItemView view, boolean animate, boolean forceExpand)1375 private void handleRowExpanded(CallLogListItemView view, boolean animate, boolean forceExpand) { 1376 final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); 1377 1378 if (forceExpand && isExpanded(views.rowId)) { 1379 return; 1380 } 1381 1382 // Hide or show the actions view. 1383 boolean expanded = toggleExpansion(views.rowId); 1384 1385 // Trigger loading of the viewstub and visual expand or collapse. 1386 expandOrCollapseActions(view, expanded); 1387 1388 // Animate the expansion or collapse. 1389 if (mCallItemExpandedListener != null) { 1390 if (animate) { 1391 mCallItemExpandedListener.onItemExpanded(view); 1392 } 1393 1394 // Animate the collapse of the previous item if it is still visible on screen. 1395 if (mPreviouslyExpanded != NONE_EXPANDED) { 1396 CallLogListItemView previousItem = mCallItemExpandedListener.getViewForCallId( 1397 mPreviouslyExpanded); 1398 1399 if (previousItem != null) { 1400 expandOrCollapseActions(previousItem, false); 1401 if (animate) { 1402 mCallItemExpandedListener.onItemExpanded(previousItem); 1403 } 1404 } 1405 mPreviouslyExpanded = NONE_EXPANDED; 1406 } 1407 } 1408 } 1409 } 1410