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 com.google.common.annotations.VisibleForTesting;
     20 
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.SharedPreferences;
     24 import android.content.res.Resources;
     25 import android.database.Cursor;
     26 import android.net.Uri;
     27 import android.os.Bundle;
     28 import android.os.Trace;
     29 import android.preference.PreferenceManager;
     30 import android.provider.CallLog;
     31 import android.provider.ContactsContract.CommonDataKinds.Phone;
     32 import android.support.v7.widget.RecyclerView;
     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.ArrayMap;
     39 import android.view.LayoutInflater;
     40 import android.view.View;
     41 import android.view.View.AccessibilityDelegate;
     42 import android.view.ViewGroup;
     43 import android.view.accessibility.AccessibilityEvent;
     44 
     45 import com.android.contacts.common.ContactsUtils;
     46 import com.android.contacts.common.compat.CompatUtils;
     47 import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
     48 import com.android.contacts.common.preference.ContactsPreferences;
     49 import com.android.contacts.common.util.PermissionsUtil;
     50 import com.android.dialer.DialtactsActivity;
     51 import com.android.dialer.PhoneCallDetails;
     52 import com.android.dialer.R;
     53 import com.android.dialer.calllog.calllogcache.CallLogCache;
     54 import com.android.dialer.contactinfo.ContactInfoCache;
     55 import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
     56 import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
     57 import com.android.dialer.database.VoicemailArchiveContract;
     58 import com.android.dialer.filterednumber.BlockNumberDialogFragment.Callback;
     59 import com.android.dialer.logging.InteractionEvent;
     60 import com.android.dialer.logging.Logger;
     61 import com.android.dialer.service.ExtendedBlockingButtonRenderer;
     62 import com.android.dialer.util.PhoneNumberUtil;
     63 import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
     64 
     65 import java.util.HashMap;
     66 import java.util.Map;
     67 
     68 /**
     69  * Adapter class to fill in data for the Call Log.
     70  */
     71 public class CallLogAdapter extends GroupingListAdapter
     72         implements CallLogGroupBuilder.GroupCreator,
     73                 VoicemailPlaybackPresenter.OnVoicemailDeletedListener,
     74                 ExtendedBlockingButtonRenderer.Listener {
     75 
     76     // Types of activities the call log adapter is used for
     77     public static final int ACTIVITY_TYPE_CALL_LOG = 1;
     78     public static final int ACTIVITY_TYPE_ARCHIVE = 2;
     79     public static final int ACTIVITY_TYPE_DIALTACTS = 3;
     80 
     81     /** Interface used to initiate a refresh of the content. */
     82     public interface CallFetcher {
     83         public void fetchCalls();
     84     }
     85 
     86     private static final int NO_EXPANDED_LIST_ITEM = -1;
     87     // ConcurrentHashMap doesn't store null values. Use this value for numbers which aren't blocked.
     88     private static final int NOT_BLOCKED = -1;
     89 
     90     private static final int VOICEMAIL_PROMO_CARD_POSITION = 0;
     91 
     92     protected static final int VIEW_TYPE_NORMAL = 0;
     93     private static final int VIEW_TYPE_VOICEMAIL_PROMO_CARD = 1;
     94 
     95     /**
     96      * The key for the show voicemail promo card preference which will determine whether the promo
     97      * card was permanently dismissed or not.
     98      */
     99     private static final String SHOW_VOICEMAIL_PROMO_CARD = "show_voicemail_promo_card";
    100     private static final boolean SHOW_VOICEMAIL_PROMO_CARD_DEFAULT = true;
    101 
    102     protected final Context mContext;
    103     private final ContactInfoHelper mContactInfoHelper;
    104     protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
    105     private final CallFetcher mCallFetcher;
    106     private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
    107     private final Map<String, Boolean> mBlockedNumberCache = new ArrayMap<>();
    108 
    109     protected ContactInfoCache mContactInfoCache;
    110 
    111     private final int mActivityType;
    112 
    113     private static final String KEY_EXPANDED_POSITION = "expanded_position";
    114     private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id";
    115 
    116     // Tracks the position of the currently expanded list item.
    117     private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
    118     // Tracks the rowId of the currently expanded list item, so the position can be updated if there
    119     // are any changes to the call log entries, such as additions or removals.
    120     private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
    121     private int mHiddenPosition = RecyclerView.NO_POSITION;
    122     private Uri mHiddenItemUri = null;
    123     private boolean mPendingHide = false;
    124 
    125     /**
    126      *  Hashmap, keyed by call Id, used to track the day group for a call.  As call log entries are
    127      *  put into the primary call groups in {@link com.android.dialer.calllog.CallLogGroupBuilder},
    128      *  they are also assigned a secondary "day group".  This hashmap tracks the day group assigned
    129      *  to all calls in the call log.  This information is used to trigger the display of a day
    130      *  group header above the call log entry at the start of a day group.
    131      *  Note: Multiple calls are grouped into a single primary "call group" in the call log, and
    132      *  the cursor used to bind rows includes all of these calls.  When determining if a day group
    133      *  change has occurred it is necessary to look at the last entry in the call log to determine
    134      *  its day group.  This hashmap provides a means of determining the previous day group without
    135      *  having to reverse the cursor to the start of the previous day call log entry.
    136      */
    137     private HashMap<Long, Integer> mDayGroups = new HashMap<>();
    138 
    139     private boolean mLoading = true;
    140 
    141     private SharedPreferences mPrefs;
    142 
    143     private ContactsPreferences mContactsPreferences;
    144 
    145     protected boolean mShowVoicemailPromoCard = false;
    146 
    147     /** Instance of helper class for managing views. */
    148     private final CallLogListItemHelper mCallLogListItemHelper;
    149 
    150     /** Cache for repeated requests to Telecom/Telephony. */
    151     protected final CallLogCache mCallLogCache;
    152 
    153     /** Helper to group call log entries. */
    154     private final CallLogGroupBuilder mCallLogGroupBuilder;
    155 
    156     /**
    157      * The OnClickListener used to expand or collapse the action buttons of a call log entry.
    158      */
    159     private final View.OnClickListener mExpandCollapseListener = new View.OnClickListener() {
    160         @Override
    161         public void onClick(View v) {
    162             CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag();
    163             if (viewHolder == null) {
    164                 return;
    165             }
    166 
    167             if (mVoicemailPlaybackPresenter != null) {
    168                 // Always reset the voicemail playback state on expand or collapse.
    169                 mVoicemailPlaybackPresenter.resetAll();
    170             }
    171 
    172             if (viewHolder.getAdapterPosition() == mCurrentlyExpandedPosition) {
    173                 // Hide actions, if the clicked item is the expanded item.
    174                 viewHolder.showActions(false);
    175 
    176                 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
    177                 mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
    178             } else {
    179                 if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) {
    180                     CallLogAsyncTaskUtil.markCallAsRead(mContext, viewHolder.callIds);
    181                     if (mActivityType == ACTIVITY_TYPE_DIALTACTS) {
    182                         ((DialtactsActivity) v.getContext()).updateTabUnreadCounts();
    183                     }
    184                 }
    185                 expandViewHolderActions(viewHolder);
    186             }
    187 
    188         }
    189     };
    190 
    191     /**
    192      * Click handler used to dismiss the promo card when the user taps the "ok" button.
    193      */
    194     private final View.OnClickListener mOkActionListener = new View.OnClickListener() {
    195         @Override
    196         public void onClick(View view) {
    197             dismissVoicemailPromoCard();
    198         }
    199     };
    200 
    201     /**
    202      * Click handler used to send the user to the voicemail settings screen and then dismiss the
    203      * promo card.
    204      */
    205     private final View.OnClickListener mVoicemailSettingsActionListener =
    206             new View.OnClickListener() {
    207         @Override
    208         public void onClick(View view) {
    209             Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
    210             mContext.startActivity(intent);
    211             dismissVoicemailPromoCard();
    212         }
    213     };
    214 
    215     private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
    216         // If another item is expanded, notify it that it has changed. Its actions will be
    217         // hidden when it is re-binded because we change mCurrentlyExpandedPosition below.
    218         if (mCurrentlyExpandedPosition != RecyclerView.NO_POSITION) {
    219             notifyItemChanged(mCurrentlyExpandedPosition);
    220         }
    221         // Show the actions for the clicked list item.
    222         viewHolder.showActions(true);
    223         mCurrentlyExpandedPosition = viewHolder.getAdapterPosition();
    224         mCurrentlyExpandedRowId = viewHolder.rowId;
    225     }
    226 
    227     /**
    228      * Expand the actions on a list item when focused in Talkback mode, to aid discoverability.
    229      */
    230     private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() {
    231         @Override
    232         public boolean onRequestSendAccessibilityEvent(
    233                 ViewGroup host, View child, AccessibilityEvent event) {
    234             if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
    235                 // Only expand if actions are not already expanded, because triggering the expand
    236                 // function on clicks causes the action views to lose the focus indicator.
    237                 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) host.getTag();
    238                 if (mCurrentlyExpandedPosition != viewHolder.getAdapterPosition()) {
    239                     if (mVoicemailPlaybackPresenter != null) {
    240                         // Always reset the voicemail playback state on expand.
    241                         mVoicemailPlaybackPresenter.resetAll();
    242                     }
    243 
    244                     expandViewHolderActions((CallLogListItemViewHolder) host.getTag());
    245                 }
    246             }
    247             return super.onRequestSendAccessibilityEvent(host, child, event);
    248         }
    249     };
    250 
    251     protected final OnContactInfoChangedListener mOnContactInfoChangedListener =
    252             new OnContactInfoChangedListener() {
    253                 @Override
    254                 public void onContactInfoChanged() {
    255                     notifyDataSetChanged();
    256                 }
    257             };
    258 
    259     public CallLogAdapter(
    260             Context context,
    261             CallFetcher callFetcher,
    262             ContactInfoHelper contactInfoHelper,
    263             VoicemailPlaybackPresenter voicemailPlaybackPresenter,
    264             int activityType) {
    265         super(context);
    266 
    267         mContext = context;
    268         mCallFetcher = callFetcher;
    269         mContactInfoHelper = contactInfoHelper;
    270         mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
    271         if (mVoicemailPlaybackPresenter != null) {
    272             mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this);
    273         }
    274 
    275         mActivityType = activityType;
    276 
    277         mContactInfoCache = new ContactInfoCache(
    278                 mContactInfoHelper, mOnContactInfoChangedListener);
    279         if (!PermissionsUtil.hasContactsPermissions(context)) {
    280             mContactInfoCache.disableRequestProcessing();
    281         }
    282 
    283         Resources resources = mContext.getResources();
    284         CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
    285 
    286         mCallLogCache = CallLogCache.getCallLogCache(mContext);
    287 
    288         PhoneCallDetailsHelper phoneCallDetailsHelper =
    289                 new PhoneCallDetailsHelper(mContext, resources, mCallLogCache);
    290         mCallLogListItemHelper =
    291                 new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache);
    292         mCallLogGroupBuilder = new CallLogGroupBuilder(this);
    293         mFilteredNumberAsyncQueryHandler =
    294                 new FilteredNumberAsyncQueryHandler(mContext.getContentResolver());
    295 
    296         mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
    297         mContactsPreferences = new ContactsPreferences(mContext);
    298         maybeShowVoicemailPromoCard();
    299     }
    300 
    301     public void onSaveInstanceState(Bundle outState) {
    302         outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition);
    303         outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId);
    304     }
    305 
    306     public void onRestoreInstanceState(Bundle savedInstanceState) {
    307         if (savedInstanceState != null) {
    308             mCurrentlyExpandedPosition =
    309                     savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION);
    310             mCurrentlyExpandedRowId =
    311                     savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM);
    312         }
    313     }
    314 
    315     @Override
    316     public void onBlockedNumber(String number,String countryIso) {
    317         String cacheKey = PhoneNumberUtils.formatNumberToE164(number, countryIso);
    318         if (!TextUtils.isEmpty(cacheKey)) {
    319             mBlockedNumberCache.put(cacheKey, true);
    320             notifyDataSetChanged();
    321         }
    322     }
    323 
    324     @Override
    325     public void onUnblockedNumber( String number, String countryIso) {
    326         String cacheKey = PhoneNumberUtils.formatNumberToE164(number, countryIso);
    327         if (!TextUtils.isEmpty(cacheKey)) {
    328             mBlockedNumberCache.put(cacheKey, false);
    329             notifyDataSetChanged();
    330         }
    331     }
    332 
    333     /**
    334      * Requery on background thread when {@link Cursor} changes.
    335      */
    336     @Override
    337     protected void onContentChanged() {
    338         mCallFetcher.fetchCalls();
    339     }
    340 
    341     public void setLoading(boolean loading) {
    342         mLoading = loading;
    343     }
    344 
    345     public boolean isEmpty() {
    346         if (mLoading) {
    347             // We don't want the empty state to show when loading.
    348             return false;
    349         } else {
    350             return getItemCount() == 0;
    351         }
    352     }
    353 
    354     public void invalidateCache() {
    355         mContactInfoCache.invalidate();
    356     }
    357 
    358     public void onResume() {
    359         if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
    360             mContactInfoCache.start();
    361         }
    362         mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
    363     }
    364 
    365     public void onPause() {
    366         pauseCache();
    367 
    368         if (mHiddenItemUri != null) {
    369             CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null);
    370         }
    371     }
    372 
    373     @VisibleForTesting
    374     /* package */ void pauseCache() {
    375         mContactInfoCache.stop();
    376         mCallLogCache.reset();
    377     }
    378 
    379     @Override
    380     protected void addGroups(Cursor cursor) {
    381         mCallLogGroupBuilder.addGroups(cursor);
    382     }
    383 
    384     @Override
    385     public void addVoicemailGroups(Cursor cursor) {
    386         mCallLogGroupBuilder.addVoicemailGroups(cursor);
    387     }
    388 
    389     @Override
    390     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    391         if (viewType == VIEW_TYPE_VOICEMAIL_PROMO_CARD) {
    392             return createVoicemailPromoCardViewHolder(parent);
    393         }
    394         return createCallLogEntryViewHolder(parent);
    395     }
    396 
    397     /**
    398      * Creates a new call log entry {@link ViewHolder}.
    399      *
    400      * @param parent the parent view.
    401      * @return The {@link ViewHolder}.
    402      */
    403     private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) {
    404         LayoutInflater inflater = LayoutInflater.from(mContext);
    405         View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
    406         CallLogListItemViewHolder viewHolder = CallLogListItemViewHolder.create(
    407                 view,
    408                 mContext,
    409                 this,
    410                 mExpandCollapseListener,
    411                 mCallLogCache,
    412                 mCallLogListItemHelper,
    413                 mVoicemailPlaybackPresenter,
    414                 mFilteredNumberAsyncQueryHandler,
    415                 new Callback() {
    416                     @Override
    417                     public void onFilterNumberSuccess() {
    418                         Logger.logInteraction(
    419                                 InteractionEvent.BLOCK_NUMBER_CALL_LOG);
    420                     }
    421 
    422                     @Override
    423                     public void onUnfilterNumberSuccess() {
    424                         Logger.logInteraction(
    425                                 InteractionEvent.UNBLOCK_NUMBER_CALL_LOG);
    426                     }
    427 
    428                     @Override
    429                     public void onChangeFilteredNumberUndo() {}
    430                 }, mActivityType == ACTIVITY_TYPE_ARCHIVE);
    431 
    432         viewHolder.callLogEntryView.setTag(viewHolder);
    433         viewHolder.callLogEntryView.setAccessibilityDelegate(mAccessibilityDelegate);
    434 
    435         viewHolder.primaryActionView.setTag(viewHolder);
    436 
    437         return viewHolder;
    438     }
    439 
    440     /**
    441      * Binds the views in the entry to the data in the call log.
    442      * TODO: This gets called 20-30 times when Dialer starts up for a single call log entry and
    443      * should not. It invokes cross-process methods and the repeat execution can get costly.
    444      *
    445      * @param viewHolder The view corresponding to this entry.
    446      * @param position The position of the entry.
    447      */
    448     @Override
    449     public void onBindViewHolder(ViewHolder viewHolder, int position) {
    450         Trace.beginSection("onBindViewHolder: " + position);
    451 
    452         switch (getItemViewType(position)) {
    453             case VIEW_TYPE_VOICEMAIL_PROMO_CARD:
    454                 bindVoicemailPromoCardViewHolder(viewHolder);
    455                 break;
    456             default:
    457                 bindCallLogListViewHolder(viewHolder, position);
    458                 break;
    459         }
    460 
    461         Trace.endSection();
    462     }
    463 
    464     /**
    465      * Binds the promo card view holder.
    466      *
    467      * @param viewHolder The promo card view holder.
    468      */
    469     protected void bindVoicemailPromoCardViewHolder(ViewHolder viewHolder) {
    470         PromoCardViewHolder promoCardViewHolder = (PromoCardViewHolder) viewHolder;
    471 
    472         promoCardViewHolder.getSecondaryActionView()
    473                 .setOnClickListener(mVoicemailSettingsActionListener);
    474         promoCardViewHolder.getPrimaryActionView().setOnClickListener(mOkActionListener);
    475     }
    476 
    477     /**
    478      * Binds the view holder for the call log list item view.
    479      *
    480      * @param viewHolder The call log list item view holder.
    481      * @param position The position of the list item.
    482      */
    483 
    484     private void bindCallLogListViewHolder(ViewHolder viewHolder, int position) {
    485         Cursor c = (Cursor) getItem(position);
    486         if (c == null) {
    487             return;
    488         }
    489 
    490         int count = getGroupSize(position);
    491 
    492         final String number = c.getString(CallLogQuery.NUMBER);
    493         final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
    494         final String postDialDigits = CompatUtils.isNCompatible()
    495                 && mActivityType != ACTIVITY_TYPE_ARCHIVE ?
    496                 c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
    497         final String viaNumber = CompatUtils.isNCompatible()
    498                 && mActivityType != ACTIVITY_TYPE_ARCHIVE ?
    499                 c.getString(CallLogQuery.VIA_NUMBER) : "";
    500         final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION);
    501         final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount(
    502                 c.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME),
    503                 c.getString(CallLogQuery.ACCOUNT_ID));
    504         final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(c);
    505         final boolean isVoicemailNumber =
    506                 mCallLogCache.isVoicemailNumber(accountHandle, number);
    507 
    508         // Note: Binding of the action buttons is done as required in configureActionViews when the
    509         // user expands the actions ViewStub.
    510 
    511         ContactInfo info = ContactInfo.EMPTY;
    512         if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !isVoicemailNumber) {
    513             // Lookup contacts with this number
    514             info = mContactInfoCache.getValue(number + postDialDigits,
    515                     countryIso, cachedContactInfo);
    516         }
    517         CharSequence formattedNumber = info.formattedNumber == null
    518                 ? null : PhoneNumberUtilsCompat.createTtsSpannable(info.formattedNumber);
    519 
    520         final PhoneCallDetails details = new PhoneCallDetails(
    521                 mContext, number, numberPresentation, formattedNumber,
    522                 postDialDigits, isVoicemailNumber);
    523         details.viaNumber = viaNumber;
    524         details.accountHandle = accountHandle;
    525         details.countryIso = countryIso;
    526         details.date = c.getLong(CallLogQuery.DATE);
    527         details.duration = c.getLong(CallLogQuery.DURATION);
    528         details.features = getCallFeatures(c, count);
    529         details.geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
    530         details.transcription = c.getString(CallLogQuery.TRANSCRIPTION);
    531         details.callTypes = getCallTypes(c, count);
    532 
    533         if (!c.isNull(CallLogQuery.DATA_USAGE)) {
    534             details.dataUsage = c.getLong(CallLogQuery.DATA_USAGE);
    535         }
    536 
    537         if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) {
    538             details.contactUri = info.lookupUri;
    539             details.namePrimary = info.name;
    540             details.nameAlternative = info.nameAlternative;
    541             details.nameDisplayOrder = mContactsPreferences.getDisplayOrder();
    542             details.numberType = info.type;
    543             details.numberLabel = info.label;
    544             details.photoUri = info.photoUri;
    545             details.sourceType = info.sourceType;
    546             details.objectId = info.objectId;
    547             details.contactUserType = info.userType;
    548         }
    549 
    550         final CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
    551         views.info = info;
    552         views.rowId = c.getLong(CallLogQuery.ID);
    553         // Store values used when the actions ViewStub is inflated on expansion.
    554         views.number = number;
    555         views.postDialDigits = details.postDialDigits;
    556         views.displayNumber = details.displayNumber;
    557         views.numberPresentation = numberPresentation;
    558 
    559         views.accountHandle = accountHandle;
    560         // Stash away the Ids of the calls so that we can support deleting a row in the call log.
    561         views.callIds = getCallIds(c, count);
    562         views.isBusiness = mContactInfoHelper.isBusiness(info.sourceType);
    563         views.numberType = (String) Phone.getTypeLabel(mContext.getResources(), details.numberType,
    564                 details.numberLabel);
    565         // Default case: an item in the call log.
    566         views.primaryActionView.setVisibility(View.VISIBLE);
    567         views.workIconView.setVisibility(
    568                 details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE);
    569 
    570         // Check if the day group has changed and display a header if necessary.
    571         int currentGroup = getDayGroupForCall(views.rowId);
    572         int previousGroup = getPreviousDayGroup(c);
    573         if (currentGroup != previousGroup) {
    574             views.dayGroupHeader.setVisibility(View.VISIBLE);
    575             views.dayGroupHeader.setText(getGroupDescription(currentGroup));
    576         } else {
    577             views.dayGroupHeader.setVisibility(View.GONE);
    578         }
    579 
    580         if (mActivityType == ACTIVITY_TYPE_ARCHIVE) {
    581             views.callType = CallLog.Calls.VOICEMAIL_TYPE;
    582             views.voicemailUri = VoicemailArchiveContract.VoicemailArchive.buildWithId(c.getInt(
    583                     c.getColumnIndex(VoicemailArchiveContract.VoicemailArchive._ID)))
    584                     .toString();
    585 
    586         } else {
    587             if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE ||
    588                     details.callTypes[0] == CallLog.Calls.MISSED_TYPE) {
    589                 details.isRead = c.getInt(CallLogQuery.IS_READ) == 1;
    590             }
    591             views.callType = c.getInt(CallLogQuery.CALL_TYPE);
    592             views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
    593         }
    594 
    595         mCallLogListItemHelper.setPhoneCallDetails(views, details);
    596 
    597         if (mCurrentlyExpandedRowId == views.rowId) {
    598             // In case ViewHolders were added/removed, update the expanded position if the rowIds
    599             // match so that we can restore the correct expanded state on rebind.
    600             mCurrentlyExpandedPosition = position;
    601             views.showActions(true);
    602         } else {
    603             views.showActions(false);
    604         }
    605         views.updatePhoto();
    606 
    607         mCallLogListItemHelper.setPhoneCallDetails(views, details);
    608     }
    609 
    610     private String getPreferredDisplayName(ContactInfo contactInfo) {
    611         if (mContactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY ||
    612                 TextUtils.isEmpty(contactInfo.nameAlternative)) {
    613             return contactInfo.name;
    614         }
    615         return contactInfo.nameAlternative;
    616     }
    617 
    618     @Override
    619     public int getItemCount() {
    620         return super.getItemCount() + (mShowVoicemailPromoCard ? 1 : 0)
    621                 - (mHiddenPosition != RecyclerView.NO_POSITION ? 1 : 0);
    622     }
    623 
    624     @Override
    625     public int getItemViewType(int position) {
    626         if (position == VOICEMAIL_PROMO_CARD_POSITION && mShowVoicemailPromoCard) {
    627             return VIEW_TYPE_VOICEMAIL_PROMO_CARD;
    628         }
    629         return super.getItemViewType(position);
    630     }
    631 
    632     /**
    633      * Retrieves an item at the specified position, taking into account the presence of a promo
    634      * card.
    635      *
    636      * @param position The position to retrieve.
    637      * @return The item at that position.
    638      */
    639     @Override
    640     public Object getItem(int position) {
    641         return super.getItem(position - (mShowVoicemailPromoCard ? 1 : 0)
    642                 + ((mHiddenPosition != RecyclerView.NO_POSITION && position >= mHiddenPosition)
    643                 ? 1 : 0));
    644     }
    645 
    646     @Override
    647     public int getGroupSize(int position) {
    648         return super.getGroupSize(position - (mShowVoicemailPromoCard ? 1 : 0));
    649     }
    650 
    651     protected boolean isCallLogActivity() {
    652         return mActivityType == ACTIVITY_TYPE_CALL_LOG;
    653     }
    654 
    655     /**
    656      * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user
    657      * clicks the delete button, the deleted item is temporarily hidden from the list. If a user
    658      * clicks delete on a second item before the first item's undo option has expired, the first
    659      * item is immediately deleted so that only one item can be "undoed" at a time.
    660      */
    661     @Override
    662     public void onVoicemailDeleted(Uri uri) {
    663         if (mHiddenItemUri == null) {
    664             // Immediately hide the currently expanded card.
    665             mHiddenPosition = mCurrentlyExpandedPosition;
    666             notifyDataSetChanged();
    667         } else {
    668             // This means that there was a previous item that was hidden in the UI but not
    669             // yet deleted from the database (call it a "pending delete"). Delete this previous item
    670             // now since it is only possible to do one "undo" at a time.
    671             CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null);
    672 
    673             // Set pending hide action so that the current item is hidden only after the previous
    674             // item is permanently deleted.
    675             mPendingHide = true;
    676         }
    677 
    678         collapseExpandedCard();
    679 
    680         // Save the new hidden item uri in case it needs to be deleted from the database when
    681         // a user attempts to delete another item.
    682         mHiddenItemUri = uri;
    683     }
    684 
    685     private void collapseExpandedCard() {
    686         mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
    687         mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
    688     }
    689 
    690     /**
    691      * When the list is changing all stored position is no longer valid.
    692      */
    693     public void invalidatePositions() {
    694         mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
    695         mHiddenPosition = RecyclerView.NO_POSITION;
    696     }
    697 
    698     /**
    699      * When the user clicks "undo", the hidden item is unhidden.
    700      */
    701     @Override
    702     public void onVoicemailDeleteUndo() {
    703         mHiddenPosition = RecyclerView.NO_POSITION;
    704         mHiddenItemUri = null;
    705 
    706         mPendingHide = false;
    707         notifyDataSetChanged();
    708     }
    709 
    710     /**
    711      * This callback signifies that a database deletion has completed. This means that if there is
    712      * an item pending deletion, it will be hidden because the previous item that was in "undo" mode
    713      * has been removed from the database. Otherwise it simply resets the hidden state because there
    714      * are no pending deletes and thus no hidden items.
    715      */
    716     @Override
    717     public void onVoicemailDeletedInDatabase() {
    718         if (mPendingHide) {
    719             mHiddenPosition = mCurrentlyExpandedPosition;
    720             mPendingHide = false;
    721         } else {
    722             // There should no longer be any hidden item because it has been deleted from the
    723             // database.
    724             mHiddenPosition = RecyclerView.NO_POSITION;
    725             mHiddenItemUri = null;
    726         }
    727     }
    728 
    729     /**
    730      * Retrieves the day group of the previous call in the call log.  Used to determine if the day
    731      * group has changed and to trigger display of the day group text.
    732      *
    733      * @param cursor The call log cursor.
    734      * @return The previous day group, or DAY_GROUP_NONE if this is the first call.
    735      */
    736     private int getPreviousDayGroup(Cursor cursor) {
    737         // We want to restore the position in the cursor at the end.
    738         int startingPosition = cursor.getPosition();
    739         int dayGroup = CallLogGroupBuilder.DAY_GROUP_NONE;
    740         if (cursor.moveToPrevious()) {
    741             // If the previous entry is hidden (deleted in the UI but not in the database), skip it
    742             // and check the card above it. A list with the voicemail promo card at the top will be
    743             // 1-indexed because the 0th index is the promo card iteself.
    744             int previousViewPosition = mShowVoicemailPromoCard ? startingPosition :
    745                 startingPosition - 1;
    746             if (previousViewPosition != mHiddenPosition ||
    747                     (previousViewPosition == mHiddenPosition && cursor.moveToPrevious())) {
    748                 long previousRowId = cursor.getLong(CallLogQuery.ID);
    749                 dayGroup = getDayGroupForCall(previousRowId);
    750             }
    751         }
    752         cursor.moveToPosition(startingPosition);
    753         return dayGroup;
    754     }
    755 
    756     /**
    757      * Given a call Id, look up the day group that the call belongs to.  The day group data is
    758      * populated in {@link com.android.dialer.calllog.CallLogGroupBuilder}.
    759      *
    760      * @param callId The call to retrieve the day group for.
    761      * @return The day group for the call.
    762      */
    763     private int getDayGroupForCall(long callId) {
    764         if (mDayGroups.containsKey(callId)) {
    765             return mDayGroups.get(callId);
    766         }
    767         return CallLogGroupBuilder.DAY_GROUP_NONE;
    768     }
    769 
    770     /**
    771      * Returns the call types for the given number of items in the cursor.
    772      * <p>
    773      * It uses the next {@code count} rows in the cursor to extract the types.
    774      * <p>
    775      * It position in the cursor is unchanged by this function.
    776      */
    777     private int[] getCallTypes(Cursor cursor, int count) {
    778         if (mActivityType == ACTIVITY_TYPE_ARCHIVE) {
    779             return new int[] {CallLog.Calls.VOICEMAIL_TYPE};
    780         }
    781         int position = cursor.getPosition();
    782         int[] callTypes = new int[count];
    783         for (int index = 0; index < count; ++index) {
    784             callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
    785             cursor.moveToNext();
    786         }
    787         cursor.moveToPosition(position);
    788         return callTypes;
    789     }
    790 
    791     /**
    792      * Determine the features which were enabled for any of the calls that make up a call log
    793      * entry.
    794      *
    795      * @param cursor The cursor.
    796      * @param count The number of calls for the current call log entry.
    797      * @return The features.
    798      */
    799     private int getCallFeatures(Cursor cursor, int count) {
    800         int features = 0;
    801         int position = cursor.getPosition();
    802         for (int index = 0; index < count; ++index) {
    803             features |= cursor.getInt(CallLogQuery.FEATURES);
    804             cursor.moveToNext();
    805         }
    806         cursor.moveToPosition(position);
    807         return features;
    808     }
    809 
    810     /**
    811      * Sets whether processing of requests for contact details should be enabled.
    812      *
    813      * This method should be called in tests to disable such processing of requests when not
    814      * needed.
    815      */
    816     @VisibleForTesting
    817     void disableRequestProcessingForTest() {
    818         // TODO: Remove this and test the cache directly.
    819         mContactInfoCache.disableRequestProcessing();
    820     }
    821 
    822     @VisibleForTesting
    823     void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
    824         // TODO: Remove this and test the cache directly.
    825         mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo);
    826     }
    827 
    828     /**
    829      * Stores the day group associated with a call in the call log.
    830      *
    831      * @param rowId The row Id of the current call.
    832      * @param dayGroup The day group the call belongs in.
    833      */
    834     @Override
    835     public void setDayGroup(long rowId, int dayGroup) {
    836         if (!mDayGroups.containsKey(rowId)) {
    837             mDayGroups.put(rowId, dayGroup);
    838         }
    839     }
    840 
    841     /**
    842      * Clears the day group associations on re-bind of the call log.
    843      */
    844     @Override
    845     public void clearDayGroups() {
    846         mDayGroups.clear();
    847     }
    848 
    849     /**
    850      * Retrieves the call Ids represented by the current call log row.
    851      *
    852      * @param cursor Call log cursor to retrieve call Ids from.
    853      * @param groupSize Number of calls associated with the current call log row.
    854      * @return Array of call Ids.
    855      */
    856     private long[] getCallIds(final Cursor cursor, final int groupSize) {
    857         // We want to restore the position in the cursor at the end.
    858         int startingPosition = cursor.getPosition();
    859         long[] ids = new long[groupSize];
    860         // Copy the ids of the rows in the group.
    861         for (int index = 0; index < groupSize; ++index) {
    862             ids[index] = cursor.getLong(CallLogQuery.ID);
    863             cursor.moveToNext();
    864         }
    865         cursor.moveToPosition(startingPosition);
    866         return ids;
    867     }
    868 
    869     /**
    870      * Determines the description for a day group.
    871      *
    872      * @param group The day group to retrieve the description for.
    873      * @return The day group description.
    874      */
    875     private CharSequence getGroupDescription(int group) {
    876        if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) {
    877            return mContext.getResources().getString(R.string.call_log_header_today);
    878        } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) {
    879            return mContext.getResources().getString(R.string.call_log_header_yesterday);
    880        } else {
    881            return mContext.getResources().getString(R.string.call_log_header_other);
    882        }
    883     }
    884 
    885     /**
    886      * Determines if the voicemail promo card should be shown or not.  The voicemail promo card will
    887      * be shown as the first item in the voicemail tab.
    888      */
    889     private void maybeShowVoicemailPromoCard() {
    890         boolean showPromoCard = mPrefs.getBoolean(SHOW_VOICEMAIL_PROMO_CARD,
    891                 SHOW_VOICEMAIL_PROMO_CARD_DEFAULT);
    892         mShowVoicemailPromoCard = mActivityType != ACTIVITY_TYPE_ARCHIVE &&
    893                 (mVoicemailPlaybackPresenter != null) && showPromoCard;
    894     }
    895 
    896     /**
    897      * Dismisses the voicemail promo card and refreshes the call log.
    898      */
    899     private void dismissVoicemailPromoCard() {
    900         mPrefs.edit().putBoolean(SHOW_VOICEMAIL_PROMO_CARD, false).apply();
    901         mShowVoicemailPromoCard = false;
    902         notifyItemRemoved(VOICEMAIL_PROMO_CARD_POSITION);
    903     }
    904 
    905     /**
    906      * Creates the view holder for the voicemail promo card.
    907      *
    908      * @param parent The parent view.
    909      * @return The {@link ViewHolder}.
    910      */
    911     protected ViewHolder createVoicemailPromoCardViewHolder(ViewGroup parent) {
    912         LayoutInflater inflater = LayoutInflater.from(mContext);
    913         View view = inflater.inflate(R.layout.voicemail_promo_card, parent, false);
    914 
    915         PromoCardViewHolder viewHolder = PromoCardViewHolder.create(view);
    916         return viewHolder;
    917     }
    918 }
    919