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.Context; 20 import android.content.Intent; 21 import android.content.SharedPreferences; 22 import android.content.res.Resources; 23 import android.database.Cursor; 24 import android.net.Uri; 25 import android.provider.ContactsContract; 26 import android.provider.ContactsContract.CommonDataKinds.Phone; 27 import android.support.v7.widget.RecyclerView; 28 import android.os.Bundle; 29 import android.os.Trace; 30 import android.preference.PreferenceActivity; 31 import android.preference.PreferenceManager; 32 import android.provider.CallLog; 33 import android.support.v7.widget.RecyclerView.ViewHolder; 34 import android.telecom.PhoneAccountHandle; 35 import android.telephony.PhoneNumberUtils; 36 import android.telephony.TelephonyManager; 37 import android.text.TextUtils; 38 import android.util.Log; 39 import android.view.ContextMenu; 40 import android.view.LayoutInflater; 41 import android.view.MenuItem; 42 import android.view.MenuItem.OnMenuItemClickListener; 43 import android.view.View; 44 import android.view.View.AccessibilityDelegate; 45 import android.view.ViewGroup; 46 import android.view.ViewTreeObserver; 47 import android.view.ContextMenu.ContextMenuInfo; 48 import android.view.accessibility.AccessibilityEvent; 49 50 import com.android.contacts.common.CallUtil; 51 import com.android.contacts.common.ClipboardUtils; 52 import com.android.contacts.common.util.PermissionsUtil; 53 import com.android.dialer.DialtactsActivity; 54 import com.android.dialer.PhoneCallDetails; 55 import com.android.dialer.R; 56 import com.android.dialer.contactinfo.ContactInfoCache; 57 import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener; 58 import com.android.dialer.util.DialerUtils; 59 import com.android.dialer.util.PhoneNumberUtil; 60 import com.android.dialer.voicemail.VoicemailPlaybackPresenter; 61 62 import com.google.common.annotations.VisibleForTesting; 63 64 import java.util.HashMap; 65 66 /** 67 * Adapter class to fill in data for the Call Log. 68 */ 69 public class CallLogAdapter extends GroupingListAdapter 70 implements CallLogGroupBuilder.GroupCreator, 71 VoicemailPlaybackPresenter.OnVoicemailDeletedListener { 72 73 /** Interface used to initiate a refresh of the content. */ 74 public interface CallFetcher { fetchCalls()75 public void fetchCalls(); 76 } 77 78 private static final int VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM = 10; 79 private static final int NO_EXPANDED_LIST_ITEM = -1; 80 81 private static final int VOICEMAIL_PROMO_CARD_POSITION = 0; 82 /** 83 * View type for voicemail promo card. Note: Numbering starts at 20 to avoid collision 84 * with {@link com.android.common.widget.GroupingListAdapter#ITEM_TYPE_IN_GROUP}, and 85 * {@link CallLogAdapter#VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM}. 86 */ 87 private static final int VIEW_TYPE_VOICEMAIL_PROMO_CARD = 20; 88 89 /** 90 * The key for the show voicemail promo card preference which will determine whether the promo 91 * card was permanently dismissed or not. 92 */ 93 private static final String SHOW_VOICEMAIL_PROMO_CARD = "show_voicemail_promo_card"; 94 private static final boolean SHOW_VOICEMAIL_PROMO_CARD_DEFAULT = true; 95 96 protected final Context mContext; 97 private final ContactInfoHelper mContactInfoHelper; 98 private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; 99 private final CallFetcher mCallFetcher; 100 101 protected ContactInfoCache mContactInfoCache; 102 103 private boolean mIsShowingRecentsTab; 104 105 private static final String KEY_EXPANDED_POSITION = "expanded_position"; 106 private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id"; 107 108 // Tracks the position of the currently expanded list item. 109 private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 110 // Tracks the rowId of the currently expanded list item, so the position can be updated if there 111 // are any changes to the call log entries, such as additions or removals. 112 private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 113 114 /** 115 * Hashmap, keyed by call Id, used to track the day group for a call. As call log entries are 116 * put into the primary call groups in {@link com.android.dialer.calllog.CallLogGroupBuilder}, 117 * they are also assigned a secondary "day group". This hashmap tracks the day group assigned 118 * to all calls in the call log. This information is used to trigger the display of a day 119 * group header above the call log entry at the start of a day group. 120 * Note: Multiple calls are grouped into a single primary "call group" in the call log, and 121 * the cursor used to bind rows includes all of these calls. When determining if a day group 122 * change has occurred it is necessary to look at the last entry in the call log to determine 123 * its day group. This hashmap provides a means of determining the previous day group without 124 * having to reverse the cursor to the start of the previous day call log entry. 125 */ 126 private HashMap<Long,Integer> mDayGroups = new HashMap<Long, Integer>(); 127 128 private boolean mLoading = true; 129 130 private SharedPreferences mPrefs; 131 132 private boolean mShowPromoCard = false; 133 134 /** Instance of helper class for managing views. */ 135 private final CallLogListItemHelper mCallLogListItemHelper; 136 137 /** Cache for repeated requests to TelecomManager. */ 138 protected final TelecomCallLogCache mTelecomCallLogCache; 139 140 /** Helper to group call log entries. */ 141 private final CallLogGroupBuilder mCallLogGroupBuilder; 142 143 /** 144 * The OnClickListener used to expand or collapse the action buttons of a call log entry. 145 */ 146 private final View.OnClickListener mExpandCollapseListener = new View.OnClickListener() { 147 @Override 148 public void onClick(View v) { 149 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag(); 150 if (viewHolder == null) { 151 return; 152 } 153 154 if (mVoicemailPlaybackPresenter != null) { 155 // Always reset the voicemail playback state on expand or collapse. 156 mVoicemailPlaybackPresenter.resetAll(); 157 } 158 159 if (viewHolder.getAdapterPosition() == mCurrentlyExpandedPosition) { 160 // Hide actions, if the clicked item is the expanded item. 161 viewHolder.showActions(false); 162 163 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 164 mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 165 } else { 166 expandViewHolderActions(viewHolder); 167 } 168 169 } 170 }; 171 172 /** 173 * Click handler used to dismiss the promo card when the user taps the "ok" button. 174 */ 175 private final View.OnClickListener mOkActionListener = new View.OnClickListener() { 176 @Override 177 public void onClick(View view) { 178 dismissVoicemailPromoCard(); 179 } 180 }; 181 182 /** 183 * Click handler used to send the user to the voicemail settings screen and then dismiss the 184 * promo card. 185 */ 186 private final View.OnClickListener mVoicemailSettingsActionListener = 187 new View.OnClickListener() { 188 @Override 189 public void onClick(View view) { 190 Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL); 191 mContext.startActivity(intent); 192 dismissVoicemailPromoCard(); 193 } 194 }; 195 196 /** 197 * Listener that is triggered to populate the context menu with actions to perform on the call's 198 * number, when the call log entry is long pressed. 199 */ 200 private final View.OnCreateContextMenuListener mOnCreateContextMenuListener = 201 new View.OnCreateContextMenuListener() { 202 @Override 203 public void onCreateContextMenu(ContextMenu menu, View v, 204 ContextMenuInfo menuInfo) { 205 final CallLogListItemViewHolder vh = 206 (CallLogListItemViewHolder) v.getTag(); 207 if (TextUtils.isEmpty(vh.number)) { 208 return; 209 } 210 211 menu.setHeaderTitle(vh.number); 212 213 final MenuItem copyItem = menu.add( 214 ContextMenu.NONE, 215 R.id.context_menu_copy_to_clipboard, 216 ContextMenu.NONE, 217 R.string.copy_text); 218 219 copyItem.setOnMenuItemClickListener(new OnMenuItemClickListener() { 220 @Override 221 public boolean onMenuItemClick(MenuItem item) { 222 ClipboardUtils.copyText(CallLogAdapter.this.mContext, null, 223 vh.number, true); 224 return true; 225 } 226 }); 227 228 // The edit number before call does not show up if any of the conditions apply: 229 // 1) Number cannot be called 230 // 2) Number is the voicemail number 231 // 3) Number is a SIP address 232 233 if (!PhoneNumberUtil.canPlaceCallsTo(vh.number, vh.numberPresentation) 234 || mTelecomCallLogCache.isVoicemailNumber(vh.accountHandle, vh.number) 235 || PhoneNumberUtil.isSipNumber(vh.number)) { 236 return; 237 } 238 239 final MenuItem editItem = menu.add( 240 ContextMenu.NONE, 241 R.id.context_menu_edit_before_call, 242 ContextMenu.NONE, 243 R.string.recentCalls_editNumberBeforeCall); 244 245 editItem.setOnMenuItemClickListener(new OnMenuItemClickListener() { 246 @Override 247 public boolean onMenuItemClick(MenuItem item) { 248 final Intent intent = new Intent(Intent.ACTION_DIAL, 249 CallUtil.getCallUri(vh.number)); 250 intent.setClass(mContext, DialtactsActivity.class); 251 DialerUtils.startActivityWithErrorToast(mContext, intent); 252 return true; 253 } 254 }); 255 } 256 }; 257 expandViewHolderActions(CallLogListItemViewHolder viewHolder)258 private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) { 259 // If another item is expanded, notify it that it has changed. Its actions will be 260 // hidden when it is re-binded because we change mCurrentlyExpandedPosition below. 261 if (mCurrentlyExpandedPosition != RecyclerView.NO_POSITION) { 262 notifyItemChanged(mCurrentlyExpandedPosition); 263 } 264 // Show the actions for the clicked list item. 265 viewHolder.showActions(true); 266 mCurrentlyExpandedPosition = viewHolder.getAdapterPosition(); 267 mCurrentlyExpandedRowId = viewHolder.rowId; 268 } 269 270 /** 271 * Expand the actions on a list item when focused in Talkback mode, to aid discoverability. 272 */ 273 private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() { 274 @Override 275 public boolean onRequestSendAccessibilityEvent( 276 ViewGroup host, View child, AccessibilityEvent event) { 277 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { 278 // Only expand if actions are not already expanded, because triggering the expand 279 // function on clicks causes the action views to lose the focus indicator. 280 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) host.getTag(); 281 if (mCurrentlyExpandedPosition != viewHolder.getAdapterPosition()) { 282 expandViewHolderActions((CallLogListItemViewHolder) host.getTag()); 283 } 284 } 285 return super.onRequestSendAccessibilityEvent(host, child, event); 286 } 287 }; 288 289 protected final OnContactInfoChangedListener mOnContactInfoChangedListener = 290 new OnContactInfoChangedListener() { 291 @Override 292 public void onContactInfoChanged() { 293 notifyDataSetChanged(); 294 } 295 }; 296 CallLogAdapter( Context context, CallFetcher callFetcher, ContactInfoHelper contactInfoHelper, VoicemailPlaybackPresenter voicemailPlaybackPresenter, boolean isShowingRecentsTab)297 public CallLogAdapter( 298 Context context, 299 CallFetcher callFetcher, 300 ContactInfoHelper contactInfoHelper, 301 VoicemailPlaybackPresenter voicemailPlaybackPresenter, 302 boolean isShowingRecentsTab) { 303 super(context); 304 305 mContext = context; 306 mCallFetcher = callFetcher; 307 mContactInfoHelper = contactInfoHelper; 308 mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; 309 if (mVoicemailPlaybackPresenter != null) { 310 mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this); 311 } 312 mIsShowingRecentsTab = isShowingRecentsTab; 313 314 mContactInfoCache = new ContactInfoCache( 315 mContactInfoHelper, mOnContactInfoChangedListener); 316 if (!PermissionsUtil.hasContactsPermissions(context)) { 317 mContactInfoCache.disableRequestProcessing(); 318 } 319 320 Resources resources = mContext.getResources(); 321 CallTypeHelper callTypeHelper = new CallTypeHelper(resources); 322 323 mTelecomCallLogCache = new TelecomCallLogCache(mContext); 324 PhoneCallDetailsHelper phoneCallDetailsHelper = 325 new PhoneCallDetailsHelper(mContext, resources, mTelecomCallLogCache); 326 mCallLogListItemHelper = 327 new CallLogListItemHelper(phoneCallDetailsHelper, resources, mTelecomCallLogCache); 328 mCallLogGroupBuilder = new CallLogGroupBuilder(this); 329 mPrefs = PreferenceManager.getDefaultSharedPreferences(context); 330 maybeShowVoicemailPromoCard(); 331 } 332 onSaveInstanceState(Bundle outState)333 public void onSaveInstanceState(Bundle outState) { 334 outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition); 335 outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId); 336 } 337 onRestoreInstanceState(Bundle savedInstanceState)338 public void onRestoreInstanceState(Bundle savedInstanceState) { 339 if (savedInstanceState != null) { 340 mCurrentlyExpandedPosition = 341 savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION); 342 mCurrentlyExpandedRowId = 343 savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM); 344 } 345 } 346 347 /** 348 * Requery on background thread when {@link Cursor} changes. 349 */ 350 @Override onContentChanged()351 protected void onContentChanged() { 352 mCallFetcher.fetchCalls(); 353 } 354 setLoading(boolean loading)355 public void setLoading(boolean loading) { 356 mLoading = loading; 357 } 358 isEmpty()359 public boolean isEmpty() { 360 if (mLoading) { 361 // We don't want the empty state to show when loading. 362 return false; 363 } else { 364 return getItemCount() == 0; 365 } 366 } 367 invalidateCache()368 public void invalidateCache() { 369 mContactInfoCache.invalidate(); 370 } 371 startCache()372 public void startCache() { 373 if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) { 374 mContactInfoCache.start(); 375 } 376 } 377 pauseCache()378 public void pauseCache() { 379 mContactInfoCache.stop(); 380 mTelecomCallLogCache.reset(); 381 } 382 383 @Override addGroups(Cursor cursor)384 protected void addGroups(Cursor cursor) { 385 mCallLogGroupBuilder.addGroups(cursor); 386 } 387 388 @Override onCreateViewHolder(ViewGroup parent, int viewType)389 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 390 if (viewType == VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM) { 391 return ShowCallHistoryViewHolder.create(mContext, parent); 392 } else if (viewType == VIEW_TYPE_VOICEMAIL_PROMO_CARD) { 393 return createVoicemailPromoCardViewHolder(parent); 394 } 395 return createCallLogEntryViewHolder(parent); 396 } 397 398 /** 399 * Creates a new call log entry {@link ViewHolder}. 400 * 401 * @param parent the parent view. 402 * @return The {@link ViewHolder}. 403 */ createCallLogEntryViewHolder(ViewGroup parent)404 private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) { 405 LayoutInflater inflater = LayoutInflater.from(mContext); 406 View view = inflater.inflate(R.layout.call_log_list_item, parent, false); 407 CallLogListItemViewHolder viewHolder = CallLogListItemViewHolder.create( 408 view, 409 mContext, 410 mExpandCollapseListener, 411 mTelecomCallLogCache, 412 mCallLogListItemHelper, 413 mVoicemailPlaybackPresenter); 414 415 viewHolder.callLogEntryView.setTag(viewHolder); 416 viewHolder.callLogEntryView.setAccessibilityDelegate(mAccessibilityDelegate); 417 418 viewHolder.primaryActionView.setOnCreateContextMenuListener(mOnCreateContextMenuListener); 419 viewHolder.primaryActionView.setTag(viewHolder); 420 421 return viewHolder; 422 } 423 424 /** 425 * Binds the views in the entry to the data in the call log. 426 * TODO: This gets called 20-30 times when Dialer starts up for a single call log entry and 427 * should not. It invokes cross-process methods and the repeat execution can get costly. 428 * 429 * @param ViewHolder The view corresponding to this entry. 430 * @param position The position of the entry. 431 */ onBindViewHolder(ViewHolder viewHolder, int position)432 public void onBindViewHolder(ViewHolder viewHolder, int position) { 433 Trace.beginSection("onBindViewHolder: " + position); 434 435 switch (getItemViewType(position)) { 436 case VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM: 437 break; 438 case VIEW_TYPE_VOICEMAIL_PROMO_CARD: 439 bindVoicemailPromoCardViewHolder(viewHolder); 440 break; 441 default: 442 bindCallLogListViewHolder(viewHolder, position); 443 break; 444 } 445 446 Trace.endSection(); 447 } 448 449 /** 450 * Binds the promo card view holder. 451 * 452 * @param viewHolder The promo card view holder. 453 */ bindVoicemailPromoCardViewHolder(ViewHolder viewHolder)454 protected void bindVoicemailPromoCardViewHolder(ViewHolder viewHolder) { 455 PromoCardViewHolder promoCardViewHolder = (PromoCardViewHolder) viewHolder; 456 457 promoCardViewHolder.getSettingsTextView().setOnClickListener( 458 mVoicemailSettingsActionListener); 459 promoCardViewHolder.getOkTextView().setOnClickListener(mOkActionListener); 460 } 461 462 /** 463 * Binds the view holder for the call log list item view. 464 * 465 * @param viewHolder The call log list item view holder. 466 * @param position The position of the list item. 467 */ 468 bindCallLogListViewHolder(ViewHolder viewHolder, int position)469 private void bindCallLogListViewHolder(ViewHolder viewHolder, int position) { 470 Cursor c = (Cursor) getItem(position); 471 if (c == null) { 472 return; 473 } 474 475 int count = getGroupSize(position); 476 477 final String number = c.getString(CallLogQuery.NUMBER); 478 final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION); 479 final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount( 480 c.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME), 481 c.getString(CallLogQuery.ACCOUNT_ID)); 482 final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO); 483 final ContactInfo cachedContactInfo = mContactInfoHelper.getContactInfo(c); 484 final boolean isVoicemailNumber = 485 mTelecomCallLogCache.isVoicemailNumber(accountHandle, number); 486 487 // Note: Binding of the action buttons is done as required in configureActionViews when the 488 // user expands the actions ViewStub. 489 490 ContactInfo info = ContactInfo.EMPTY; 491 if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !isVoicemailNumber) { 492 // Lookup contacts with this number 493 info = mContactInfoCache.getValue(number, countryIso, cachedContactInfo); 494 } 495 CharSequence formattedNumber = info.formattedNumber == null 496 ? null : PhoneNumberUtils.createTtsSpannable(info.formattedNumber); 497 498 final PhoneCallDetails details = new PhoneCallDetails( 499 mContext, number, numberPresentation, formattedNumber, isVoicemailNumber); 500 details.accountHandle = accountHandle; 501 details.callTypes = getCallTypes(c, count); 502 details.countryIso = countryIso; 503 details.date = c.getLong(CallLogQuery.DATE); 504 details.duration = c.getLong(CallLogQuery.DURATION); 505 details.features = getCallFeatures(c, count); 506 details.geocode = c.getString(CallLogQuery.GEOCODED_LOCATION); 507 details.transcription = c.getString(CallLogQuery.TRANSCRIPTION); 508 if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE) { 509 details.isRead = c.getInt(CallLogQuery.IS_READ) == 1; 510 } 511 512 if (!c.isNull(CallLogQuery.DATA_USAGE)) { 513 details.dataUsage = c.getLong(CallLogQuery.DATA_USAGE); 514 } 515 516 if (!TextUtils.isEmpty(info.name)) { 517 details.contactUri = info.lookupUri; 518 details.name = info.name; 519 details.numberType = info.type; 520 details.numberLabel = info.label; 521 details.photoUri = info.photoUri; 522 details.sourceType = info.sourceType; 523 details.objectId = info.objectId; 524 } 525 526 CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; 527 views.info = info; 528 views.rowId = c.getLong(CallLogQuery.ID); 529 // Store values used when the actions ViewStub is inflated on expansion. 530 views.number = number; 531 views.displayNumber = details.displayNumber; 532 views.numberPresentation = numberPresentation; 533 views.callType = c.getInt(CallLogQuery.CALL_TYPE); 534 views.accountHandle = accountHandle; 535 views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); 536 // Stash away the Ids of the calls so that we can support deleting a row in the call log. 537 views.callIds = getCallIds(c, count); 538 views.isBusiness = mContactInfoHelper.isBusiness(info.sourceType); 539 views.numberType = (String) Phone.getTypeLabel(mContext.getResources(), details.numberType, 540 details.numberLabel); 541 // Default case: an item in the call log. 542 views.primaryActionView.setVisibility(View.VISIBLE); 543 544 // Check if the day group has changed and display a header if necessary. 545 int currentGroup = getDayGroupForCall(views.rowId); 546 int previousGroup = getPreviousDayGroup(c); 547 if (currentGroup != previousGroup) { 548 views.dayGroupHeader.setVisibility(View.VISIBLE); 549 views.dayGroupHeader.setText(getGroupDescription(currentGroup)); 550 } else { 551 views.dayGroupHeader.setVisibility(View.GONE); 552 } 553 554 mCallLogListItemHelper.setPhoneCallDetails(views, details); 555 556 if (mCurrentlyExpandedRowId == views.rowId) { 557 // In case ViewHolders were added/removed, update the expanded position if the rowIds 558 // match so that we can restore the correct expanded state on rebind. 559 mCurrentlyExpandedPosition = position; 560 } 561 562 views.showActions(mCurrentlyExpandedPosition == position); 563 564 String nameForDefaultImage = null; 565 if (TextUtils.isEmpty(info.name)) { 566 nameForDefaultImage = details.displayNumber; 567 } else { 568 nameForDefaultImage = info.name; 569 } 570 views.setPhoto(info.photoId, info.photoUri, info.lookupUri, nameForDefaultImage, 571 isVoicemailNumber, views.isBusiness); 572 573 mCallLogListItemHelper.setPhoneCallDetails(views, details); 574 } 575 576 @Override getItemCount()577 public int getItemCount() { 578 return super.getItemCount() + ((isShowingRecentsTab() || mShowPromoCard) ? 1 : 0); 579 } 580 581 @Override getItemViewType(int position)582 public int getItemViewType(int position) { 583 if (position == getItemCount() - 1 && isShowingRecentsTab()) { 584 return VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM; 585 } else if (position == VOICEMAIL_PROMO_CARD_POSITION && mShowPromoCard) { 586 return VIEW_TYPE_VOICEMAIL_PROMO_CARD; 587 } 588 return super.getItemViewType(position); 589 } 590 591 /** 592 * Retrieves an item at the specified position, taking into account the presence of a promo 593 * card. 594 * 595 * @param position The position to retrieve. 596 * @return The item at that position. 597 */ 598 @Override getItem(int position)599 public Object getItem(int position) { 600 return super.getItem(position - (mShowPromoCard ? 1 : 0)); 601 } 602 isShowingRecentsTab()603 protected boolean isShowingRecentsTab() { 604 return mIsShowingRecentsTab; 605 } 606 607 @Override onVoicemailDeleted(Uri uri)608 public void onVoicemailDeleted(Uri uri) { 609 mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 610 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 611 } 612 613 /** 614 * Retrieves the day group of the previous call in the call log. Used to determine if the day 615 * group has changed and to trigger display of the day group text. 616 * 617 * @param cursor The call log cursor. 618 * @return The previous day group, or DAY_GROUP_NONE if this is the first call. 619 */ getPreviousDayGroup(Cursor cursor)620 private int getPreviousDayGroup(Cursor cursor) { 621 // We want to restore the position in the cursor at the end. 622 int startingPosition = cursor.getPosition(); 623 int dayGroup = CallLogGroupBuilder.DAY_GROUP_NONE; 624 if (cursor.moveToPrevious()) { 625 long previousRowId = cursor.getLong(CallLogQuery.ID); 626 dayGroup = getDayGroupForCall(previousRowId); 627 } 628 cursor.moveToPosition(startingPosition); 629 return dayGroup; 630 } 631 632 /** 633 * Given a call Id, look up the day group that the call belongs to. The day group data is 634 * populated in {@link com.android.dialer.calllog.CallLogGroupBuilder}. 635 * 636 * @param callId The call to retrieve the day group for. 637 * @return The day group for the call. 638 */ getDayGroupForCall(long callId)639 private int getDayGroupForCall(long callId) { 640 if (mDayGroups.containsKey(callId)) { 641 return mDayGroups.get(callId); 642 } 643 return CallLogGroupBuilder.DAY_GROUP_NONE; 644 } 645 646 /** 647 * Returns the call types for the given number of items in the cursor. 648 * <p> 649 * It uses the next {@code count} rows in the cursor to extract the types. 650 * <p> 651 * It position in the cursor is unchanged by this function. 652 */ getCallTypes(Cursor cursor, int count)653 private int[] getCallTypes(Cursor cursor, int count) { 654 int position = cursor.getPosition(); 655 int[] callTypes = new int[count]; 656 for (int index = 0; index < count; ++index) { 657 callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); 658 cursor.moveToNext(); 659 } 660 cursor.moveToPosition(position); 661 return callTypes; 662 } 663 664 /** 665 * Determine the features which were enabled for any of the calls that make up a call log 666 * entry. 667 * 668 * @param cursor The cursor. 669 * @param count The number of calls for the current call log entry. 670 * @return The features. 671 */ getCallFeatures(Cursor cursor, int count)672 private int getCallFeatures(Cursor cursor, int count) { 673 int features = 0; 674 int position = cursor.getPosition(); 675 for (int index = 0; index < count; ++index) { 676 features |= cursor.getInt(CallLogQuery.FEATURES); 677 cursor.moveToNext(); 678 } 679 cursor.moveToPosition(position); 680 return features; 681 } 682 683 /** 684 * Sets whether processing of requests for contact details should be enabled. 685 * 686 * This method should be called in tests to disable such processing of requests when not 687 * needed. 688 */ 689 @VisibleForTesting disableRequestProcessingForTest()690 void disableRequestProcessingForTest() { 691 // TODO: Remove this and test the cache directly. 692 mContactInfoCache.disableRequestProcessing(); 693 } 694 695 @VisibleForTesting injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo)696 void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { 697 // TODO: Remove this and test the cache directly. 698 mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo); 699 } 700 701 @Override addGroup(int cursorPosition, int size, boolean expanded)702 public void addGroup(int cursorPosition, int size, boolean expanded) { 703 super.addGroup(cursorPosition, size, expanded); 704 } 705 706 /** 707 * Stores the day group associated with a call in the call log. 708 * 709 * @param rowId The row Id of the current call. 710 * @param dayGroup The day group the call belongs in. 711 */ 712 @Override setDayGroup(long rowId, int dayGroup)713 public void setDayGroup(long rowId, int dayGroup) { 714 if (!mDayGroups.containsKey(rowId)) { 715 mDayGroups.put(rowId, dayGroup); 716 } 717 } 718 719 /** 720 * Clears the day group associations on re-bind of the call log. 721 */ 722 @Override clearDayGroups()723 public void clearDayGroups() { 724 mDayGroups.clear(); 725 } 726 727 /** 728 * Retrieves the call Ids represented by the current call log row. 729 * 730 * @param cursor Call log cursor to retrieve call Ids from. 731 * @param groupSize Number of calls associated with the current call log row. 732 * @return Array of call Ids. 733 */ getCallIds(final Cursor cursor, final int groupSize)734 private long[] getCallIds(final Cursor cursor, final int groupSize) { 735 // We want to restore the position in the cursor at the end. 736 int startingPosition = cursor.getPosition(); 737 long[] ids = new long[groupSize]; 738 // Copy the ids of the rows in the group. 739 for (int index = 0; index < groupSize; ++index) { 740 ids[index] = cursor.getLong(CallLogQuery.ID); 741 cursor.moveToNext(); 742 } 743 cursor.moveToPosition(startingPosition); 744 return ids; 745 } 746 747 /** 748 * Determines the description for a day group. 749 * 750 * @param group The day group to retrieve the description for. 751 * @return The day group description. 752 */ getGroupDescription(int group)753 private CharSequence getGroupDescription(int group) { 754 if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) { 755 return mContext.getResources().getString(R.string.call_log_header_today); 756 } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) { 757 return mContext.getResources().getString(R.string.call_log_header_yesterday); 758 } else { 759 return mContext.getResources().getString(R.string.call_log_header_other); 760 } 761 } 762 763 /** 764 * Determines if the voicemail promo card should be shown or not. The voicemail promo card will 765 * be shown as the first item in the voicemail tab. 766 */ maybeShowVoicemailPromoCard()767 private void maybeShowVoicemailPromoCard() { 768 boolean showPromoCard = mPrefs.getBoolean(SHOW_VOICEMAIL_PROMO_CARD, 769 SHOW_VOICEMAIL_PROMO_CARD_DEFAULT); 770 mShowPromoCard = (mVoicemailPlaybackPresenter != null) && showPromoCard; 771 } 772 773 /** 774 * Dismisses the voicemail promo card and refreshes the call log. 775 */ dismissVoicemailPromoCard()776 private void dismissVoicemailPromoCard() { 777 mPrefs.edit().putBoolean(SHOW_VOICEMAIL_PROMO_CARD, false).apply(); 778 mShowPromoCard = false; 779 notifyItemRemoved(VOICEMAIL_PROMO_CARD_POSITION); 780 } 781 782 /** 783 * Creates the view holder for the voicemail promo card. 784 * 785 * @param parent The parent view. 786 * @return The {@link ViewHolder}. 787 */ createVoicemailPromoCardViewHolder(ViewGroup parent)788 protected ViewHolder createVoicemailPromoCardViewHolder(ViewGroup parent) { 789 LayoutInflater inflater = LayoutInflater.from(mContext); 790 View view = inflater.inflate(R.layout.voicemail_promo_card, parent, false); 791 792 PromoCardViewHolder viewHolder = PromoCardViewHolder.create(view); 793 return viewHolder; 794 } 795 } 796