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