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