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.app.calllog;
     18 
     19 import android.app.Activity;
     20 import android.content.ContentUris;
     21 import android.content.DialogInterface;
     22 import android.content.res.Resources;
     23 import android.database.Cursor;
     24 import android.net.Uri;
     25 import android.os.AsyncTask;
     26 import android.os.Build.VERSION;
     27 import android.os.Build.VERSION_CODES;
     28 import android.os.Bundle;
     29 import android.os.Trace;
     30 import android.provider.CallLog;
     31 import android.provider.ContactsContract.CommonDataKinds.Phone;
     32 import android.support.annotation.MainThread;
     33 import android.support.annotation.NonNull;
     34 import android.support.annotation.Nullable;
     35 import android.support.annotation.VisibleForTesting;
     36 import android.support.annotation.WorkerThread;
     37 import android.support.v7.app.AlertDialog;
     38 import android.support.v7.widget.RecyclerView;
     39 import android.support.v7.widget.RecyclerView.ViewHolder;
     40 import android.telecom.PhoneAccountHandle;
     41 import android.text.TextUtils;
     42 import android.util.ArrayMap;
     43 import android.util.ArraySet;
     44 import android.util.SparseArray;
     45 import android.view.ActionMode;
     46 import android.view.LayoutInflater;
     47 import android.view.Menu;
     48 import android.view.MenuInflater;
     49 import android.view.MenuItem;
     50 import android.view.View;
     51 import android.view.ViewGroup;
     52 import com.android.contacts.common.ContactsUtils;
     53 import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
     54 import com.android.contacts.common.preference.ContactsPreferences;
     55 import com.android.dialer.app.Bindings;
     56 import com.android.dialer.app.DialtactsActivity;
     57 import com.android.dialer.app.R;
     58 import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator;
     59 import com.android.dialer.app.calllog.calllogcache.CallLogCache;
     60 import com.android.dialer.app.contactinfo.ContactInfoCache;
     61 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
     62 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDeletedListener;
     63 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
     64 import com.android.dialer.calldetails.CallDetailsEntries;
     65 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
     66 import com.android.dialer.calllogutils.PhoneAccountUtils;
     67 import com.android.dialer.calllogutils.PhoneCallDetails;
     68 import com.android.dialer.common.Assert;
     69 import com.android.dialer.common.ConfigProviderBindings;
     70 import com.android.dialer.common.LogUtil;
     71 import com.android.dialer.common.concurrent.AsyncTaskExecutor;
     72 import com.android.dialer.common.concurrent.AsyncTaskExecutors;
     73 import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
     74 import com.android.dialer.enrichedcall.EnrichedCallComponent;
     75 import com.android.dialer.enrichedcall.EnrichedCallManager;
     76 import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult;
     77 import com.android.dialer.lightbringer.Lightbringer;
     78 import com.android.dialer.lightbringer.LightbringerComponent;
     79 import com.android.dialer.lightbringer.LightbringerListener;
     80 import com.android.dialer.logging.ContactSource;
     81 import com.android.dialer.logging.DialerImpression;
     82 import com.android.dialer.logging.Logger;
     83 import com.android.dialer.phonenumbercache.CallLogQuery;
     84 import com.android.dialer.phonenumbercache.ContactInfo;
     85 import com.android.dialer.phonenumbercache.ContactInfoHelper;
     86 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
     87 import com.android.dialer.spam.Spam;
     88 import com.android.dialer.util.PermissionsUtil;
     89 import java.util.Collections;
     90 import java.util.List;
     91 import java.util.Map;
     92 import java.util.Set;
     93 
     94 /** Adapter class to fill in data for the Call Log. */
     95 public class CallLogAdapter extends GroupingListAdapter
     96     implements GroupCreator, OnVoicemailDeletedListener, LightbringerListener {
     97 
     98   // Types of activities the call log adapter is used for
     99   public static final int ACTIVITY_TYPE_CALL_LOG = 1;
    100   public static final int ACTIVITY_TYPE_DIALTACTS = 2;
    101   private static final int NO_EXPANDED_LIST_ITEM = -1;
    102   public static final int ALERT_POSITION = 0;
    103   private static final int VIEW_TYPE_ALERT = 1;
    104   private static final int VIEW_TYPE_CALLLOG = 2;
    105 
    106   private static final String KEY_EXPANDED_POSITION = "expanded_position";
    107   private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id";
    108 
    109   public static final String LOAD_DATA_TASK_IDENTIFIER = "load_data";
    110 
    111   public static final String ENABLE_CALL_LOG_MULTI_SELECT = "enable_call_log_multiselect";
    112   public static final boolean ENABLE_CALL_LOG_MULTI_SELECT_FLAG = false;
    113 
    114   protected final Activity mActivity;
    115   protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
    116   /** Cache for repeated requests to Telecom/Telephony. */
    117   protected final CallLogCache mCallLogCache;
    118 
    119   private final CallFetcher mCallFetcher;
    120   @NonNull private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
    121   private final int mActivityType;
    122 
    123   /** Instance of helper class for managing views. */
    124   private final CallLogListItemHelper mCallLogListItemHelper;
    125   /** Helper to group call log entries. */
    126   private final CallLogGroupBuilder mCallLogGroupBuilder;
    127 
    128   private final AsyncTaskExecutor mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
    129   private ContactInfoCache mContactInfoCache;
    130   // Tracks the position of the currently expanded list item.
    131   private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
    132   // Tracks the rowId of the currently expanded list item, so the position can be updated if there
    133   // are any changes to the call log entries, such as additions or removals.
    134   private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
    135 
    136   private final CallLogAlertManager mCallLogAlertManager;
    137 
    138   public ActionMode mActionMode = null;
    139   private final SparseArray<String> selectedItems = new SparseArray<>();
    140 
    141   private final ActionMode.Callback mActionModeCallback =
    142       new ActionMode.Callback() {
    143 
    144         // Called when the action mode is created; startActionMode() was called
    145         @Override
    146         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    147           mActionMode = mode;
    148           // Inflate a menu resource providing context menu items
    149           MenuInflater inflater = mode.getMenuInflater();
    150           inflater.inflate(R.menu.actionbar_delete, menu);
    151           return true;
    152         }
    153 
    154         // Called each time the action mode is shown. Always called after onCreateActionMode, but
    155         // may be called multiple times if the mode is invalidated.
    156         @Override
    157         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    158           return false; // Return false if nothing is done
    159         }
    160 
    161         // Called when the user selects a contextual menu item
    162         @Override
    163         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    164           if (item.getItemId() == R.id.action_bar_delete_menu_item) {
    165             if (selectedItems.size() > 0) {
    166               showDeleteSelectedItemsDialog();
    167             }
    168             mode.finish();
    169             return true;
    170           } else {
    171             return false;
    172           }
    173         }
    174 
    175         // Called when the user exits the action mode
    176         @Override
    177         public void onDestroyActionMode(ActionMode mode) {
    178           selectedItems.clear();
    179           mActionMode = null;
    180           notifyDataSetChanged();
    181         }
    182       };
    183 
    184   // Todo (uabdullah): Use plurals http://b/37751831
    185   private void showDeleteSelectedItemsDialog() {
    186     AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
    187     Assert.checkArgument(selectedItems.size() > 0);
    188     String voicemailString =
    189         selectedItems.size() == 1
    190             ? mActivity.getResources().getString(R.string.voicemailMultiSelectVoicemail)
    191             : mActivity.getResources().getString(R.string.voicemailMultiSelectVoicemails);
    192     String deleteVoicemailTitle =
    193         mActivity
    194             .getResources()
    195             .getString(R.string.voicemailMultiSelectDialogTitle, voicemailString);
    196     SparseArray<String> voicemailsToDeleteOnConfirmation = selectedItems.clone();
    197     builder.setTitle(deleteVoicemailTitle);
    198 
    199     builder.setPositiveButton(
    200         mActivity.getResources().getString(R.string.voicemailMultiSelectDeleteConfirm),
    201         new DialogInterface.OnClickListener() {
    202           @Override
    203           public void onClick(DialogInterface dialog, int id) {
    204             deleteSelectedItems(voicemailsToDeleteOnConfirmation);
    205             dialog.cancel();
    206           }
    207         });
    208 
    209     builder.setNegativeButton(
    210         mActivity.getResources().getString(R.string.voicemailMultiSelectDeleteCancel),
    211         new DialogInterface.OnClickListener() {
    212           @Override
    213           public void onClick(DialogInterface dialog, int id) {
    214             dialog.cancel();
    215           }
    216         });
    217 
    218     AlertDialog dialog = builder.create();
    219     dialog.show();
    220   }
    221 
    222   private void deleteSelectedItems(SparseArray<String> voicemailsToDelete) {
    223     for (int i = 0; i < voicemailsToDelete.size(); i++) {
    224       String voicemailUri = voicemailsToDelete.get(voicemailsToDelete.keyAt(i));
    225       CallLogAsyncTaskUtil.deleteVoicemail(mActivity, Uri.parse(voicemailUri), null);
    226     }
    227   }
    228 
    229   private final View.OnLongClickListener mLongPressListener =
    230       new View.OnLongClickListener() {
    231         @Override
    232         public boolean onLongClick(View v) {
    233           if (ConfigProviderBindings.get(v.getContext())
    234                   .getBoolean(ENABLE_CALL_LOG_MULTI_SELECT, ENABLE_CALL_LOG_MULTI_SELECT_FLAG)
    235               && mVoicemailPlaybackPresenter != null) {
    236             if (v.getId() == R.id.primary_action_view || v.getId() == R.id.quick_contact_photo) {
    237               if (mActionMode == null) {
    238                 mActionMode = v.startActionMode(mActionModeCallback);
    239               }
    240               CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag();
    241               viewHolder.quickContactView.setVisibility(View.GONE);
    242               viewHolder.checkBoxView.setVisibility(View.VISIBLE);
    243               mExpandCollapseListener.onClick(v);
    244               return true;
    245             }
    246           }
    247           return true;
    248         }
    249       };
    250 
    251   /** The OnClickListener used to expand or collapse the action buttons of a call log entry. */
    252   private final View.OnClickListener mExpandCollapseListener =
    253       new View.OnClickListener() {
    254         @Override
    255         public void onClick(View v) {
    256           CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag();
    257           if (viewHolder == null) {
    258             return;
    259           }
    260           if (mActionMode != null && viewHolder.voicemailUri != null) {
    261             int id = getVoicemailId(viewHolder.voicemailUri);
    262             if (selectedItems.get(id) != null) {
    263               selectedItems.delete(id);
    264               viewHolder.checkBoxView.setVisibility(View.GONE);
    265               viewHolder.quickContactView.setVisibility(View.VISIBLE);
    266             } else {
    267               viewHolder.quickContactView.setVisibility(View.GONE);
    268               viewHolder.checkBoxView.setVisibility(View.VISIBLE);
    269               selectedItems.put(getVoicemailId(viewHolder.voicemailUri), viewHolder.voicemailUri);
    270             }
    271 
    272             if (selectedItems.size() == 0) {
    273               mActionMode.finish();
    274               return;
    275             }
    276             mActionMode.setTitle(Integer.toString(selectedItems.size()));
    277             return;
    278           }
    279 
    280           if (mVoicemailPlaybackPresenter != null) {
    281             // Always reset the voicemail playback state on expand or collapse.
    282             mVoicemailPlaybackPresenter.resetAll();
    283           }
    284 
    285           // If enriched call capabilities were unknown on the initial load,
    286           // viewHolder.isCallComposerCapable may be unset. Check here if we have the capabilities
    287           // as a last attempt at getting them before showing the expanded view to the user
    288           EnrichedCallCapabilities capabilities =
    289               getEnrichedCallManager().getCapabilities(viewHolder.number);
    290           viewHolder.isCallComposerCapable =
    291               capabilities != null && capabilities.supportsCallComposer();
    292           generateAndMapNewCallDetailsEntriesHistoryResults(
    293               viewHolder.number,
    294               viewHolder.getDetailedPhoneDetails(),
    295               getAllHistoricalData(viewHolder.number, viewHolder.getDetailedPhoneDetails()));
    296 
    297           if (viewHolder.rowId == mCurrentlyExpandedRowId) {
    298             // Hide actions, if the clicked item is the expanded item.
    299             viewHolder.showActions(false);
    300 
    301             mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
    302             mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
    303           } else {
    304             if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) {
    305               CallLogAsyncTaskUtil.markCallAsRead(mActivity, viewHolder.callIds);
    306               if (mActivityType == ACTIVITY_TYPE_DIALTACTS) {
    307                 ((DialtactsActivity) v.getContext()).updateTabUnreadCounts();
    308               }
    309             }
    310             expandViewHolderActions(viewHolder);
    311           }
    312         }
    313       };
    314 
    315   private static int getVoicemailId(String voicemailUri) {
    316     Assert.checkArgument(voicemailUri != null);
    317     Assert.checkArgument(voicemailUri.length() > 0);
    318     return (int) ContentUris.parseId(Uri.parse(voicemailUri));
    319   }
    320 
    321   /**
    322    * A list of {@link CallLogQuery#ID} that will be hidden. The hide might be temporary so instead
    323    * if removing an item, it will be shown as an invisible view. This simplifies the calculation of
    324    * item position.
    325    */
    326   @NonNull private Set<Long> mHiddenRowIds = new ArraySet<>();
    327   /**
    328    * Holds a list of URIs that are pending deletion or undo. If the activity ends before the undo
    329    * timeout, all of the pending URIs will be deleted.
    330    *
    331    * <p>TODO: move this and OnVoicemailDeletedListener to somewhere like {@link
    332    * VisualVoicemailCallLogFragment}. The CallLogAdapter does not need to know about what to do with
    333    * hidden item or what to hide.
    334    */
    335   @NonNull private final Set<Uri> mHiddenItemUris = new ArraySet<>();
    336 
    337   private CallLogListItemViewHolder.OnClickListener mBlockReportSpamListener;
    338   /**
    339    * Map, keyed by call Id, used to track the day group for a call. As call log entries are put into
    340    * the primary call groups in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}, they are
    341    * also assigned a secondary "day group". This map tracks the day group assigned to all calls in
    342    * the call log. This information is used to trigger the display of a day group header above the
    343    * call log entry at the start of a day group. Note: Multiple calls are grouped into a single
    344    * primary "call group" in the call log, and the cursor used to bind rows includes all of these
    345    * calls. When determining if a day group change has occurred it is necessary to look at the last
    346    * entry in the call log to determine its day group. This map provides a means of determining the
    347    * previous day group without having to reverse the cursor to the start of the previous day call
    348    * log entry.
    349    */
    350   private Map<Long, Integer> mDayGroups = new ArrayMap<>();
    351 
    352   private boolean mLoading = true;
    353   private ContactsPreferences mContactsPreferences;
    354 
    355   private boolean mIsSpamEnabled;
    356 
    357   public CallLogAdapter(
    358       Activity activity,
    359       ViewGroup alertContainer,
    360       CallFetcher callFetcher,
    361       CallLogCache callLogCache,
    362       ContactInfoCache contactInfoCache,
    363       VoicemailPlaybackPresenter voicemailPlaybackPresenter,
    364       @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
    365       int activityType) {
    366     super();
    367 
    368     mActivity = activity;
    369     mCallFetcher = callFetcher;
    370     mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
    371     if (mVoicemailPlaybackPresenter != null) {
    372       mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this);
    373     }
    374 
    375     mActivityType = activityType;
    376 
    377     mContactInfoCache = contactInfoCache;
    378 
    379     if (!PermissionsUtil.hasContactsReadPermissions(activity)) {
    380       mContactInfoCache.disableRequestProcessing();
    381     }
    382 
    383     Resources resources = mActivity.getResources();
    384 
    385     mCallLogCache = callLogCache;
    386 
    387     PhoneCallDetailsHelper phoneCallDetailsHelper =
    388         new PhoneCallDetailsHelper(mActivity, resources, mCallLogCache);
    389     mCallLogListItemHelper =
    390         new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache);
    391     mCallLogGroupBuilder = new CallLogGroupBuilder(this);
    392     mFilteredNumberAsyncQueryHandler = Assert.isNotNull(filteredNumberAsyncQueryHandler);
    393 
    394     mContactsPreferences = new ContactsPreferences(mActivity);
    395 
    396     mBlockReportSpamListener =
    397         new BlockReportSpamListener(
    398             mActivity,
    399             ((Activity) mActivity).getFragmentManager(),
    400             this,
    401             mFilteredNumberAsyncQueryHandler);
    402     setHasStableIds(true);
    403 
    404     mCallLogAlertManager =
    405         new CallLogAlertManager(this, LayoutInflater.from(mActivity), alertContainer);
    406   }
    407 
    408   private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
    409     if (!TextUtils.isEmpty(viewHolder.voicemailUri)) {
    410       Logger.get(mActivity).logImpression(DialerImpression.Type.VOICEMAIL_EXPAND_ENTRY);
    411     }
    412 
    413     int lastExpandedPosition = mCurrentlyExpandedPosition;
    414     // Show the actions for the clicked list item.
    415     viewHolder.showActions(true);
    416     mCurrentlyExpandedPosition = viewHolder.getAdapterPosition();
    417     mCurrentlyExpandedRowId = viewHolder.rowId;
    418 
    419     // If another item is expanded, notify it that it has changed. Its actions will be
    420     // hidden when it is re-binded because we change mCurrentlyExpandedRowId above.
    421     if (lastExpandedPosition != RecyclerView.NO_POSITION) {
    422       notifyItemChanged(lastExpandedPosition);
    423     }
    424   }
    425 
    426   public void onSaveInstanceState(Bundle outState) {
    427     outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition);
    428     outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId);
    429   }
    430 
    431   public void onRestoreInstanceState(Bundle savedInstanceState) {
    432     if (savedInstanceState != null) {
    433       mCurrentlyExpandedPosition =
    434           savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION);
    435       mCurrentlyExpandedRowId =
    436           savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM);
    437     }
    438   }
    439 
    440   /** Requery on background thread when {@link Cursor} changes. */
    441   @Override
    442   protected void onContentChanged() {
    443     mCallFetcher.fetchCalls();
    444   }
    445 
    446   public void setLoading(boolean loading) {
    447     mLoading = loading;
    448   }
    449 
    450   public boolean isEmpty() {
    451     if (mLoading) {
    452       // We don't want the empty state to show when loading.
    453       return false;
    454     } else {
    455       return getItemCount() == 0;
    456     }
    457   }
    458 
    459   public void clearFilteredNumbersCache() {
    460     mFilteredNumberAsyncQueryHandler.clearCache();
    461   }
    462 
    463   public void onResume() {
    464     if (PermissionsUtil.hasPermission(mActivity, android.Manifest.permission.READ_CONTACTS)) {
    465       mContactInfoCache.start();
    466     }
    467     mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
    468     mIsSpamEnabled = Spam.get(mActivity).isSpamEnabled();
    469     getLightbringer().registerListener(this);
    470     notifyDataSetChanged();
    471   }
    472 
    473   public void onPause() {
    474     getLightbringer().unregisterListener(this);
    475     pauseCache();
    476     for (Uri uri : mHiddenItemUris) {
    477       CallLogAsyncTaskUtil.deleteVoicemail(mActivity, uri, null);
    478     }
    479   }
    480 
    481   public void onStop() {
    482     getEnrichedCallManager().clearCachedData();
    483   }
    484 
    485   public CallLogAlertManager getAlertManager() {
    486     return mCallLogAlertManager;
    487   }
    488 
    489   @VisibleForTesting
    490   /* package */ void pauseCache() {
    491     mContactInfoCache.stop();
    492     mCallLogCache.reset();
    493   }
    494 
    495   @Override
    496   protected void addGroups(Cursor cursor) {
    497     mCallLogGroupBuilder.addGroups(cursor);
    498   }
    499 
    500   @Override
    501   public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    502     if (viewType == VIEW_TYPE_ALERT) {
    503       return mCallLogAlertManager.createViewHolder(parent);
    504     }
    505     return createCallLogEntryViewHolder(parent);
    506   }
    507 
    508   /**
    509    * Creates a new call log entry {@link ViewHolder}.
    510    *
    511    * @param parent the parent view.
    512    * @return The {@link ViewHolder}.
    513    */
    514   private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) {
    515     LayoutInflater inflater = LayoutInflater.from(mActivity);
    516     View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
    517     CallLogListItemViewHolder viewHolder =
    518         CallLogListItemViewHolder.create(
    519             view,
    520             mActivity,
    521             mBlockReportSpamListener,
    522             mExpandCollapseListener,
    523             mLongPressListener,
    524             mCallLogCache,
    525             mCallLogListItemHelper,
    526             mVoicemailPlaybackPresenter);
    527 
    528     viewHolder.callLogEntryView.setTag(viewHolder);
    529 
    530     viewHolder.primaryActionView.setTag(viewHolder);
    531     viewHolder.quickContactView.setTag(viewHolder);
    532 
    533     return viewHolder;
    534   }
    535 
    536   /**
    537    * Binds the views in the entry to the data in the call log. TODO: This gets called 20-30 times
    538    * when Dialer starts up for a single call log entry and should not. It invokes cross-process
    539    * methods and the repeat execution can get costly.
    540    *
    541    * @param viewHolder The view corresponding to this entry.
    542    * @param position The position of the entry.
    543    */
    544   @Override
    545   public void onBindViewHolder(ViewHolder viewHolder, int position) {
    546     Trace.beginSection("onBindViewHolder: " + position);
    547     switch (getItemViewType(position)) {
    548       case VIEW_TYPE_ALERT:
    549         //Do nothing
    550         break;
    551       default:
    552         bindCallLogListViewHolder(viewHolder, position);
    553         break;
    554     }
    555     Trace.endSection();
    556   }
    557 
    558   @Override
    559   public void onViewRecycled(ViewHolder viewHolder) {
    560     if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
    561       CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
    562       if (views.asyncTask != null) {
    563         views.asyncTask.cancel(true);
    564       }
    565     }
    566   }
    567 
    568   @Override
    569   public void onViewAttachedToWindow(ViewHolder viewHolder) {
    570     if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
    571       ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = true;
    572     }
    573   }
    574 
    575   @Override
    576   public void onViewDetachedFromWindow(ViewHolder viewHolder) {
    577     if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
    578       ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = false;
    579     }
    580   }
    581 
    582   /**
    583    * Binds the view holder for the call log list item view.
    584    *
    585    * @param viewHolder The call log list item view holder.
    586    * @param position The position of the list item.
    587    */
    588   private void bindCallLogListViewHolder(final ViewHolder viewHolder, final int position) {
    589     Cursor c = (Cursor) getItem(position);
    590     if (c == null) {
    591       return;
    592     }
    593     CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
    594     views.isLoaded = false;
    595     int groupSize = getGroupSize(position);
    596     CallDetailsEntries callDetailsEntries = createCallDetailsEntries(c, groupSize);
    597     PhoneCallDetails details = createPhoneCallDetails(c, groupSize, views);
    598     if (mHiddenRowIds.contains(c.getLong(CallLogQuery.ID))) {
    599       views.callLogEntryView.setVisibility(View.GONE);
    600       views.dayGroupHeader.setVisibility(View.GONE);
    601       return;
    602     } else {
    603       views.callLogEntryView.setVisibility(View.VISIBLE);
    604       // dayGroupHeader will be restored after loadAndRender() if it is needed.
    605     }
    606     if (mCurrentlyExpandedRowId == views.rowId) {
    607       views.inflateActionViewStub();
    608     }
    609     loadAndRender(views, views.rowId, details, callDetailsEntries);
    610   }
    611 
    612   private void loadAndRender(
    613       final CallLogListItemViewHolder views,
    614       final long rowId,
    615       final PhoneCallDetails details,
    616       final CallDetailsEntries callDetailsEntries) {
    617     LogUtil.d("CallLogAdapter.loadAndRender", "position: %d", views.getAdapterPosition());
    618     // Reset block and spam information since this view could be reused which may contain
    619     // outdated data.
    620     views.isSpam = false;
    621     views.blockId = null;
    622     views.isSpamFeatureEnabled = false;
    623 
    624     // Attempt to set the isCallComposerCapable field. If capabilities are unknown for this number,
    625     // the value will be false while capabilities are requested. mExpandCollapseListener will
    626     // attempt to set the field properly in that case
    627     views.isCallComposerCapable = isCallComposerCapable(views.number);
    628     CallDetailsEntries updatedCallDetailsEntries =
    629         generateAndMapNewCallDetailsEntriesHistoryResults(
    630             views.number,
    631             callDetailsEntries,
    632             getAllHistoricalData(views.number, callDetailsEntries));
    633     views.setDetailedPhoneDetails(updatedCallDetailsEntries);
    634     views.lightbringerReady = getLightbringer().isReachable(mActivity, views.number);
    635     final AsyncTask<Void, Void, Boolean> loadDataTask =
    636         new AsyncTask<Void, Void, Boolean>() {
    637           @Override
    638           protected Boolean doInBackground(Void... params) {
    639             views.blockId =
    640                 mFilteredNumberAsyncQueryHandler.getBlockedIdSynchronous(
    641                     views.number, views.countryIso);
    642             details.isBlocked = views.blockId != null;
    643             if (isCancelled()) {
    644               return false;
    645             }
    646             if (mIsSpamEnabled) {
    647               views.isSpamFeatureEnabled = true;
    648               // Only display the call as a spam call if there are incoming calls in the list.
    649               // Call log cards with only outgoing calls should never be displayed as spam.
    650               views.isSpam =
    651                   details.hasIncomingCalls()
    652                       && Spam.get(mActivity)
    653                           .checkSpamStatusSynchronous(views.number, views.countryIso);
    654               details.isSpam = views.isSpam;
    655             }
    656             return !isCancelled() && loadData(views, rowId, details);
    657           }
    658 
    659           @Override
    660           protected void onPostExecute(Boolean success) {
    661             views.isLoaded = true;
    662             if (success) {
    663               int currentGroup = getDayGroupForCall(views.rowId);
    664               if (currentGroup != details.previousGroup) {
    665                 views.dayGroupHeaderVisibility = View.VISIBLE;
    666                 views.dayGroupHeaderText = getGroupDescription(currentGroup);
    667               } else {
    668                 views.dayGroupHeaderVisibility = View.GONE;
    669               }
    670               render(views, details, rowId);
    671             }
    672           }
    673         };
    674 
    675     views.asyncTask = loadDataTask;
    676     mAsyncTaskExecutor.submit(LOAD_DATA_TASK_IDENTIFIER, loadDataTask);
    677   }
    678 
    679   @MainThread
    680   private boolean isCallComposerCapable(@Nullable String number) {
    681     if (number == null) {
    682       return false;
    683     }
    684 
    685     EnrichedCallCapabilities capabilities = getEnrichedCallManager().getCapabilities(number);
    686     if (capabilities == null) {
    687       getEnrichedCallManager().requestCapabilities(number);
    688       return false;
    689     }
    690     return capabilities.supportsCallComposer();
    691   }
    692 
    693   @NonNull
    694   private Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData(
    695       @Nullable String number, @NonNull CallDetailsEntries entries) {
    696     if (number == null) {
    697       return Collections.emptyMap();
    698     }
    699 
    700     Map<CallDetailsEntry, List<HistoryResult>> historicalData =
    701         getEnrichedCallManager().getAllHistoricalData(number, entries);
    702     if (historicalData == null) {
    703       getEnrichedCallManager().requestAllHistoricalData(number, entries);
    704       return Collections.emptyMap();
    705     }
    706     return historicalData;
    707   }
    708 
    709   private static CallDetailsEntries generateAndMapNewCallDetailsEntriesHistoryResults(
    710       @Nullable String number,
    711       @NonNull CallDetailsEntries callDetailsEntries,
    712       @NonNull Map<CallDetailsEntry, List<HistoryResult>> mappedResults) {
    713     if (number == null) {
    714       return callDetailsEntries;
    715     }
    716     CallDetailsEntries.Builder mutableCallDetailsEntries = CallDetailsEntries.newBuilder();
    717     for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) {
    718       CallDetailsEntry.Builder newEntry = CallDetailsEntry.newBuilder().mergeFrom(entry);
    719       List<HistoryResult> results = mappedResults.get(entry);
    720       if (results != null) {
    721         newEntry.addAllHistoryResults(mappedResults.get(entry));
    722         LogUtil.v(
    723             "CallLogAdapter.generateAndMapNewCallDetailsEntriesHistoryResults",
    724             "mapped %d results",
    725             newEntry.getHistoryResultsList().size());
    726       }
    727       mutableCallDetailsEntries.addEntries(newEntry.build());
    728     }
    729     return mutableCallDetailsEntries.build();
    730   }
    731 
    732   /**
    733    * Initialize PhoneCallDetails by reading all data from cursor. This method must be run on main
    734    * thread since cursor is not thread safe.
    735    */
    736   @MainThread
    737   private PhoneCallDetails createPhoneCallDetails(
    738       Cursor cursor, int count, final CallLogListItemViewHolder views) {
    739     Assert.isMainThread();
    740     final String number = cursor.getString(CallLogQuery.NUMBER);
    741     final String postDialDigits =
    742         (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
    743     final String viaNumber =
    744         (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : "";
    745     final int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION);
    746     final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(cursor);
    747     final PhoneCallDetails details =
    748         new PhoneCallDetails(number, numberPresentation, postDialDigits);
    749     details.viaNumber = viaNumber;
    750     details.countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
    751     details.date = cursor.getLong(CallLogQuery.DATE);
    752     details.duration = cursor.getLong(CallLogQuery.DURATION);
    753     details.features = getCallFeatures(cursor, count);
    754     details.geocode = cursor.getString(CallLogQuery.GEOCODED_LOCATION);
    755     details.transcription = cursor.getString(CallLogQuery.TRANSCRIPTION);
    756     details.callTypes = getCallTypes(cursor, count);
    757 
    758     details.accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
    759     details.accountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
    760     details.cachedContactInfo = cachedContactInfo;
    761 
    762     if (!cursor.isNull(CallLogQuery.DATA_USAGE)) {
    763       details.dataUsage = cursor.getLong(CallLogQuery.DATA_USAGE);
    764     }
    765 
    766     views.rowId = cursor.getLong(CallLogQuery.ID);
    767     // Stash away the Ids of the calls so that we can support deleting a row in the call log.
    768     views.callIds = getCallIds(cursor, count);
    769     details.previousGroup = getPreviousDayGroup(cursor);
    770 
    771     // Store values used when the actions ViewStub is inflated on expansion.
    772     views.number = number;
    773     views.countryIso = details.countryIso;
    774     views.postDialDigits = details.postDialDigits;
    775     views.numberPresentation = numberPresentation;
    776 
    777     if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE
    778         || details.callTypes[0] == CallLog.Calls.MISSED_TYPE) {
    779       details.isRead = cursor.getInt(CallLogQuery.IS_READ) == 1;
    780     }
    781     views.callType = cursor.getInt(CallLogQuery.CALL_TYPE);
    782     views.voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI);
    783 
    784     return details;
    785   }
    786 
    787   @MainThread
    788   private static CallDetailsEntries createCallDetailsEntries(Cursor cursor, int count) {
    789     Assert.isMainThread();
    790     int position = cursor.getPosition();
    791     CallDetailsEntries.Builder entries = CallDetailsEntries.newBuilder();
    792     for (int i = 0; i < count; i++) {
    793       CallDetailsEntry.Builder entry =
    794           CallDetailsEntry.newBuilder()
    795               .setCallId(cursor.getLong(CallLogQuery.ID))
    796               .setCallType(cursor.getInt(CallLogQuery.CALL_TYPE))
    797               .setDataUsage(cursor.getLong(CallLogQuery.DATA_USAGE))
    798               .setDate(cursor.getLong(CallLogQuery.DATE))
    799               .setDuration(cursor.getLong(CallLogQuery.DURATION))
    800               .setFeatures(cursor.getInt(CallLogQuery.FEATURES));
    801       entries.addEntries(entry.build());
    802       cursor.moveToNext();
    803     }
    804     cursor.moveToPosition(position);
    805     return entries.build();
    806   }
    807 
    808   /**
    809    * Load data for call log. Any expensive operation should be put here to avoid blocking main
    810    * thread. Do NOT put any cursor operation here since it's not thread safe.
    811    */
    812   @WorkerThread
    813   private boolean loadData(CallLogListItemViewHolder views, long rowId, PhoneCallDetails details) {
    814     Assert.isWorkerThread();
    815     if (rowId != views.rowId) {
    816       LogUtil.i(
    817           "CallLogAdapter.loadData",
    818           "rowId of viewHolder changed after load task is issued, aborting load");
    819       return false;
    820     }
    821 
    822     final PhoneAccountHandle accountHandle =
    823         PhoneAccountUtils.getAccount(details.accountComponentName, details.accountId);
    824 
    825     final boolean isVoicemailNumber =
    826         mCallLogCache.isVoicemailNumber(accountHandle, details.number);
    827 
    828     // Note: Binding of the action buttons is done as required in configureActionViews when the
    829     // user expands the actions ViewStub.
    830 
    831     ContactInfo info = ContactInfo.EMPTY;
    832     if (PhoneNumberHelper.canPlaceCallsTo(details.number, details.numberPresentation)
    833         && !isVoicemailNumber) {
    834       // Lookup contacts with this number
    835       // Only do remote lookup in first 5 rows.
    836       int position = views.getAdapterPosition();
    837       info =
    838           mContactInfoCache.getValue(
    839               details.number + details.postDialDigits,
    840               details.countryIso,
    841               details.cachedContactInfo,
    842               position
    843                   < Bindings.get(mActivity)
    844                       .getConfigProvider()
    845                       .getLong("number_of_call_to_do_remote_lookup", 5L));
    846     }
    847     CharSequence formattedNumber =
    848         info.formattedNumber == null
    849             ? null
    850             : PhoneNumberUtilsCompat.createTtsSpannable(info.formattedNumber);
    851     details.updateDisplayNumber(mActivity, formattedNumber, isVoicemailNumber);
    852 
    853     views.displayNumber = details.displayNumber;
    854     views.accountHandle = accountHandle;
    855     details.accountHandle = accountHandle;
    856 
    857     if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) {
    858       details.contactUri = info.lookupUri;
    859       details.namePrimary = info.name;
    860       details.nameAlternative = info.nameAlternative;
    861       details.nameDisplayOrder = mContactsPreferences.getDisplayOrder();
    862       details.numberType = info.type;
    863       details.numberLabel = info.label;
    864       details.photoUri = info.photoUri;
    865       details.sourceType = info.sourceType;
    866       details.objectId = info.objectId;
    867       details.contactUserType = info.userType;
    868     }
    869     LogUtil.d(
    870         "CallLogAdapter.loadData",
    871         "position:%d, update geo info: %s, cequint caller id geo: %s, photo uri: %s <- %s",
    872         views.getAdapterPosition(),
    873         details.geocode,
    874         info.geoDescription,
    875         details.photoUri,
    876         info.photoUri);
    877     if (!TextUtils.isEmpty(info.geoDescription)) {
    878       details.geocode = info.geoDescription;
    879     }
    880 
    881     views.info = info;
    882     views.numberType = getNumberType(mActivity.getResources(), details);
    883 
    884     mCallLogListItemHelper.updatePhoneCallDetails(details);
    885     return true;
    886   }
    887 
    888   private static String getNumberType(Resources res, PhoneCallDetails details) {
    889     // Label doesn't make much sense if the information is coming from CNAP or Cequint Caller ID.
    890     if (details.sourceType == ContactSource.Type.SOURCE_TYPE_CNAP
    891         || details.sourceType == ContactSource.Type.SOURCE_TYPE_CEQUINT_CALLER_ID) {
    892       return "";
    893     }
    894     // Returns empty label instead of "custom" if the custom label is empty.
    895     if (details.numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(details.numberLabel)) {
    896       return "";
    897     }
    898     return (String) Phone.getTypeLabel(res, details.numberType, details.numberLabel);
    899   }
    900 
    901   /**
    902    * Render item view given position. This is running on UI thread so DO NOT put any expensive
    903    * operation into it.
    904    */
    905   @MainThread
    906   private void render(CallLogListItemViewHolder views, PhoneCallDetails details, long rowId) {
    907     Assert.isMainThread();
    908     if (rowId != views.rowId) {
    909       LogUtil.i(
    910           "CallLogAdapter.render",
    911           "rowId of viewHolder changed after load task is issued, aborting render");
    912       return;
    913     }
    914 
    915     // Default case: an item in the call log.
    916     views.primaryActionView.setVisibility(View.VISIBLE);
    917     views.workIconView.setVisibility(
    918         details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE);
    919 
    920     if (views.voicemailUri != null
    921         && selectedItems.get(getVoicemailId(views.voicemailUri)) != null) {
    922       views.checkBoxView.setVisibility(View.VISIBLE);
    923       views.quickContactView.setVisibility(View.GONE);
    924     } else if (views.voicemailUri != null) {
    925       views.checkBoxView.setVisibility(View.GONE);
    926       views.quickContactView.setVisibility(View.VISIBLE);
    927     }
    928 
    929     mCallLogListItemHelper.setPhoneCallDetails(views, details);
    930     if (mCurrentlyExpandedRowId == views.rowId) {
    931       // In case ViewHolders were added/removed, update the expanded position if the rowIds
    932       // match so that we can restore the correct expanded state on rebind.
    933       mCurrentlyExpandedPosition = views.getAdapterPosition();
    934       views.showActions(true);
    935     } else {
    936       views.showActions(false);
    937     }
    938     views.dayGroupHeader.setVisibility(views.dayGroupHeaderVisibility);
    939     views.dayGroupHeader.setText(views.dayGroupHeaderText);
    940   }
    941 
    942   @Override
    943   public int getItemCount() {
    944     return super.getItemCount() + (mCallLogAlertManager.isEmpty() ? 0 : 1);
    945   }
    946 
    947   @Override
    948   public int getItemViewType(int position) {
    949     if (position == ALERT_POSITION && !mCallLogAlertManager.isEmpty()) {
    950       return VIEW_TYPE_ALERT;
    951     }
    952     return VIEW_TYPE_CALLLOG;
    953   }
    954 
    955   /**
    956    * Retrieves an item at the specified position, taking into account the presence of a promo card.
    957    *
    958    * @param position The position to retrieve.
    959    * @return The item at that position.
    960    */
    961   @Override
    962   public Object getItem(int position) {
    963     return super.getItem(position - (mCallLogAlertManager.isEmpty() ? 0 : 1));
    964   }
    965 
    966   @Override
    967   public long getItemId(int position) {
    968     Cursor cursor = (Cursor) getItem(position);
    969     if (cursor != null) {
    970       return cursor.getLong(CallLogQuery.ID);
    971     } else {
    972       return 0;
    973     }
    974   }
    975 
    976   @Override
    977   public int getGroupSize(int position) {
    978     return super.getGroupSize(position - (mCallLogAlertManager.isEmpty() ? 0 : 1));
    979   }
    980 
    981   protected boolean isCallLogActivity() {
    982     return mActivityType == ACTIVITY_TYPE_CALL_LOG;
    983   }
    984 
    985   /**
    986    * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user
    987    * clicks the delete button, the deleted item is temporarily hidden from the list. If a user
    988    * clicks delete on a second item before the first item's undo option has expired, the first item
    989    * is immediately deleted so that only one item can be "undoed" at a time.
    990    */
    991   @Override
    992   public void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri) {
    993     mHiddenRowIds.add(viewHolder.rowId);
    994     // Save the new hidden item uri in case the activity is suspend before the undo has timed out.
    995     mHiddenItemUris.add(uri);
    996 
    997     collapseExpandedCard();
    998     notifyItemChanged(viewHolder.getAdapterPosition());
    999     // The next item might have to update its day group label
   1000     notifyItemChanged(viewHolder.getAdapterPosition() + 1);
   1001   }
   1002 
   1003   private void collapseExpandedCard() {
   1004     mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
   1005     mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
   1006   }
   1007 
   1008   /** When the list is changing all stored position is no longer valid. */
   1009   public void invalidatePositions() {
   1010     mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
   1011   }
   1012 
   1013   /** When the user clicks "undo", the hidden item is unhidden. */
   1014   @Override
   1015   public void onVoicemailDeleteUndo(long rowId, int adapterPosition, Uri uri) {
   1016     mHiddenItemUris.remove(uri);
   1017     mHiddenRowIds.remove(rowId);
   1018     notifyItemChanged(adapterPosition);
   1019     // The next item might have to update its day group label
   1020     notifyItemChanged(adapterPosition + 1);
   1021   }
   1022 
   1023   /** This callback signifies that a database deletion has completed. */
   1024   @Override
   1025   public void onVoicemailDeletedInDatabase(long rowId, Uri uri) {
   1026     mHiddenItemUris.remove(uri);
   1027   }
   1028 
   1029   /**
   1030    * Retrieves the day group of the previous call in the call log. Used to determine if the day
   1031    * group has changed and to trigger display of the day group text.
   1032    *
   1033    * @param cursor The call log cursor.
   1034    * @return The previous day group, or DAY_GROUP_NONE if this is the first call.
   1035    */
   1036   private int getPreviousDayGroup(Cursor cursor) {
   1037     // We want to restore the position in the cursor at the end.
   1038     int startingPosition = cursor.getPosition();
   1039     moveToPreviousNonHiddenRow(cursor);
   1040     if (cursor.isBeforeFirst()) {
   1041       cursor.moveToPosition(startingPosition);
   1042       return CallLogGroupBuilder.DAY_GROUP_NONE;
   1043     }
   1044     int result = getDayGroupForCall(cursor.getLong(CallLogQuery.ID));
   1045     cursor.moveToPosition(startingPosition);
   1046     return result;
   1047   }
   1048 
   1049   private void moveToPreviousNonHiddenRow(Cursor cursor) {
   1050     while (cursor.moveToPrevious() && mHiddenRowIds.contains(cursor.getLong(CallLogQuery.ID))) {}
   1051   }
   1052 
   1053   /**
   1054    * Given a call Id, look up the day group that the call belongs to. The day group data is
   1055    * populated in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}.
   1056    *
   1057    * @param callId The call to retrieve the day group for.
   1058    * @return The day group for the call.
   1059    */
   1060   @MainThread
   1061   private int getDayGroupForCall(long callId) {
   1062     Integer result = mDayGroups.get(callId);
   1063     if (result != null) {
   1064       return result;
   1065     }
   1066     return CallLogGroupBuilder.DAY_GROUP_NONE;
   1067   }
   1068 
   1069   /**
   1070    * Returns the call types for the given number of items in the cursor.
   1071    *
   1072    * <p>It uses the next {@code count} rows in the cursor to extract the types.
   1073    *
   1074    * <p>It position in the cursor is unchanged by this function.
   1075    */
   1076   private static int[] getCallTypes(Cursor cursor, int count) {
   1077     int position = cursor.getPosition();
   1078     int[] callTypes = new int[count];
   1079     for (int index = 0; index < count; ++index) {
   1080       callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
   1081       cursor.moveToNext();
   1082     }
   1083     cursor.moveToPosition(position);
   1084     return callTypes;
   1085   }
   1086 
   1087   /**
   1088    * Determine the features which were enabled for any of the calls that make up a call log entry.
   1089    *
   1090    * @param cursor The cursor.
   1091    * @param count The number of calls for the current call log entry.
   1092    * @return The features.
   1093    */
   1094   private int getCallFeatures(Cursor cursor, int count) {
   1095     int features = 0;
   1096     int position = cursor.getPosition();
   1097     for (int index = 0; index < count; ++index) {
   1098       features |= cursor.getInt(CallLogQuery.FEATURES);
   1099       cursor.moveToNext();
   1100     }
   1101     cursor.moveToPosition(position);
   1102     return features;
   1103   }
   1104 
   1105   /**
   1106    * Sets whether processing of requests for contact details should be enabled.
   1107    *
   1108    * <p>This method should be called in tests to disable such processing of requests when not
   1109    * needed.
   1110    */
   1111   @VisibleForTesting
   1112   void disableRequestProcessingForTest() {
   1113     // TODO: Remove this and test the cache directly.
   1114     mContactInfoCache.disableRequestProcessing();
   1115   }
   1116 
   1117   @VisibleForTesting
   1118   void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
   1119     // TODO: Remove this and test the cache directly.
   1120     mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo);
   1121   }
   1122 
   1123   /**
   1124    * Stores the day group associated with a call in the call log.
   1125    *
   1126    * @param rowId The row Id of the current call.
   1127    * @param dayGroup The day group the call belongs in.
   1128    */
   1129   @Override
   1130   @MainThread
   1131   public void setDayGroup(long rowId, int dayGroup) {
   1132     if (!mDayGroups.containsKey(rowId)) {
   1133       mDayGroups.put(rowId, dayGroup);
   1134     }
   1135   }
   1136 
   1137   /** Clears the day group associations on re-bind of the call log. */
   1138   @Override
   1139   @MainThread
   1140   public void clearDayGroups() {
   1141     mDayGroups.clear();
   1142   }
   1143 
   1144   /**
   1145    * Retrieves the call Ids represented by the current call log row.
   1146    *
   1147    * @param cursor Call log cursor to retrieve call Ids from.
   1148    * @param groupSize Number of calls associated with the current call log row.
   1149    * @return Array of call Ids.
   1150    */
   1151   private long[] getCallIds(final Cursor cursor, final int groupSize) {
   1152     // We want to restore the position in the cursor at the end.
   1153     int startingPosition = cursor.getPosition();
   1154     long[] ids = new long[groupSize];
   1155     // Copy the ids of the rows in the group.
   1156     for (int index = 0; index < groupSize; ++index) {
   1157       ids[index] = cursor.getLong(CallLogQuery.ID);
   1158       cursor.moveToNext();
   1159     }
   1160     cursor.moveToPosition(startingPosition);
   1161     return ids;
   1162   }
   1163 
   1164   /**
   1165    * Determines the description for a day group.
   1166    *
   1167    * @param group The day group to retrieve the description for.
   1168    * @return The day group description.
   1169    */
   1170   private CharSequence getGroupDescription(int group) {
   1171     if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) {
   1172       return mActivity.getResources().getString(R.string.call_log_header_today);
   1173     } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) {
   1174       return mActivity.getResources().getString(R.string.call_log_header_yesterday);
   1175     } else {
   1176       return mActivity.getResources().getString(R.string.call_log_header_other);
   1177     }
   1178   }
   1179 
   1180   @NonNull
   1181   private EnrichedCallManager getEnrichedCallManager() {
   1182     return EnrichedCallComponent.get(mActivity).getEnrichedCallManager();
   1183   }
   1184 
   1185   @NonNull
   1186   private Lightbringer getLightbringer() {
   1187     return LightbringerComponent.get(mActivity).getLightbringer();
   1188   }
   1189 
   1190   @Override
   1191   public void onLightbringerStateChanged() {
   1192     notifyDataSetChanged();
   1193   }
   1194 
   1195   /** Interface used to initiate a refresh of the content. */
   1196   public interface CallFetcher {
   1197 
   1198     void fetchCalls();
   1199   }
   1200 }
   1201