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.DialogInterface.OnCancelListener;
     23 import android.content.res.Resources;
     24 import android.database.Cursor;
     25 import android.net.Uri;
     26 import android.os.AsyncTask;
     27 import android.os.Build.VERSION;
     28 import android.os.Build.VERSION_CODES;
     29 import android.os.Bundle;
     30 import android.os.Trace;
     31 import android.provider.CallLog;
     32 import android.provider.ContactsContract.CommonDataKinds.Phone;
     33 import android.support.annotation.MainThread;
     34 import android.support.annotation.NonNull;
     35 import android.support.annotation.Nullable;
     36 import android.support.annotation.VisibleForTesting;
     37 import android.support.annotation.WorkerThread;
     38 import android.support.v7.app.AlertDialog;
     39 import android.support.v7.widget.RecyclerView;
     40 import android.support.v7.widget.RecyclerView.ViewHolder;
     41 import android.telecom.PhoneAccountHandle;
     42 import android.telephony.PhoneNumberUtils;
     43 import android.text.TextUtils;
     44 import android.util.ArrayMap;
     45 import android.util.ArraySet;
     46 import android.util.SparseArray;
     47 import android.view.ActionMode;
     48 import android.view.LayoutInflater;
     49 import android.view.Menu;
     50 import android.view.MenuInflater;
     51 import android.view.MenuItem;
     52 import android.view.View;
     53 import android.view.ViewGroup;
     54 import com.android.contacts.common.ContactsUtils;
     55 import com.android.contacts.common.preference.ContactsPreferences;
     56 import com.android.dialer.app.DialtactsActivity;
     57 import com.android.dialer.app.R;
     58 import com.android.dialer.app.calllog.CallLogFragment.CallLogFragmentListener;
     59 import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator;
     60 import com.android.dialer.app.calllog.calllogcache.CallLogCache;
     61 import com.android.dialer.app.contactinfo.ContactInfoCache;
     62 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
     63 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDeletedListener;
     64 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
     65 import com.android.dialer.calldetails.CallDetailsEntries;
     66 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
     67 import com.android.dialer.callintent.CallIntentBuilder;
     68 import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
     69 import com.android.dialer.calllogutils.PhoneCallDetails;
     70 import com.android.dialer.common.Assert;
     71 import com.android.dialer.common.FragmentUtils.FragmentUtilListener;
     72 import com.android.dialer.common.LogUtil;
     73 import com.android.dialer.common.concurrent.AsyncTaskExecutor;
     74 import com.android.dialer.common.concurrent.AsyncTaskExecutors;
     75 import com.android.dialer.compat.android.provider.VoicemailCompat;
     76 import com.android.dialer.configprovider.ConfigProviderBindings;
     77 import com.android.dialer.duo.Duo;
     78 import com.android.dialer.duo.DuoComponent;
     79 import com.android.dialer.duo.DuoConstants;
     80 import com.android.dialer.duo.DuoListener;
     81 import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
     82 import com.android.dialer.enrichedcall.EnrichedCallComponent;
     83 import com.android.dialer.enrichedcall.EnrichedCallManager;
     84 import com.android.dialer.logging.ContactSource;
     85 import com.android.dialer.logging.DialerImpression;
     86 import com.android.dialer.logging.Logger;
     87 import com.android.dialer.logging.UiAction;
     88 import com.android.dialer.main.MainActivityPeer;
     89 import com.android.dialer.performancereport.PerformanceReport;
     90 import com.android.dialer.phonenumbercache.CallLogQuery;
     91 import com.android.dialer.phonenumbercache.ContactInfo;
     92 import com.android.dialer.phonenumbercache.ContactInfoHelper;
     93 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
     94 import com.android.dialer.spam.SpamComponent;
     95 import com.android.dialer.telecom.TelecomUtil;
     96 import com.android.dialer.util.PermissionsUtil;
     97 import java.util.ArrayList;
     98 import java.util.Map;
     99 import java.util.Set;
    100 
    101 /** Adapter class to fill in data for the Call Log. */
    102 public class CallLogAdapter extends GroupingListAdapter
    103     implements GroupCreator, OnVoicemailDeletedListener, DuoListener {
    104 
    105   // Types of activities the call log adapter is used for
    106   public static final int ACTIVITY_TYPE_CALL_LOG = 1;
    107   public static final int ACTIVITY_TYPE_DIALTACTS = 2;
    108   private static final int NO_EXPANDED_LIST_ITEM = -1;
    109   public static final int ALERT_POSITION = 0;
    110   private static final int VIEW_TYPE_ALERT = 1;
    111   private static final int VIEW_TYPE_CALLLOG = 2;
    112 
    113   private static final String KEY_EXPANDED_POSITION = "expanded_position";
    114   private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id";
    115   private static final String KEY_ACTION_MODE = "action_mode_selected_items";
    116 
    117   public static final String LOAD_DATA_TASK_IDENTIFIER = "load_data";
    118 
    119   public static final String ENABLE_CALL_LOG_MULTI_SELECT = "enable_call_log_multiselect";
    120   public static final boolean ENABLE_CALL_LOG_MULTI_SELECT_FLAG = true;
    121 
    122   protected final Activity activity;
    123   protected final VoicemailPlaybackPresenter voicemailPlaybackPresenter;
    124   /** Cache for repeated requests to Telecom/Telephony. */
    125   protected final CallLogCache callLogCache;
    126 
    127   private final CallFetcher callFetcher;
    128   private final OnActionModeStateChangedListener actionModeStateChangedListener;
    129   private final MultiSelectRemoveView multiSelectRemoveView;
    130   @NonNull private final FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler;
    131   private final int activityType;
    132 
    133   /** Instance of helper class for managing views. */
    134   private final CallLogListItemHelper callLogListItemHelper;
    135   /** Helper to group call log entries. */
    136   private final CallLogGroupBuilder callLogGroupBuilder;
    137 
    138   private final AsyncTaskExecutor asyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
    139   private ContactInfoCache contactInfoCache;
    140   // Tracks the position of the currently expanded list item.
    141   private int currentlyExpandedPosition = RecyclerView.NO_POSITION;
    142   // Tracks the rowId of the currently expanded list item, so the position can be updated if there
    143   // are any changes to the call log entries, such as additions or removals.
    144   private long currentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
    145 
    146   private final CallLogAlertManager callLogAlertManager;
    147 
    148   public ActionMode actionMode = null;
    149   public boolean selectAllMode = false;
    150   public boolean deselectAllMode = false;
    151   private final SparseArray<String> selectedItems = new SparseArray<>();
    152 
    153   private final ActionMode.Callback actionModeCallback =
    154       new ActionMode.Callback() {
    155 
    156         // Called when the action mode is created; startActionMode() was called
    157         @Override
    158         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    159           if (activity != null) {
    160             announceforAccessibility(
    161                 activity.getCurrentFocus(),
    162                 activity.getString(R.string.description_entering_bulk_action_mode));
    163           }
    164           actionMode = mode;
    165           // Inflate a menu resource providing context menu items
    166           MenuInflater inflater = mode.getMenuInflater();
    167           inflater.inflate(R.menu.actionbar_delete, menu);
    168           multiSelectRemoveView.showMultiSelectRemoveView(true);
    169           actionModeStateChangedListener.onActionModeStateChanged(true);
    170           return true;
    171         }
    172 
    173         // Called each time the action mode is shown. Always called after onCreateActionMode, but
    174         // may be called multiple times if the mode is invalidated.
    175         @Override
    176         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
    177           return false; // Return false if nothing is done
    178         }
    179 
    180         // Called when the user selects a contextual menu item
    181         @Override
    182         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    183           if (item.getItemId() == R.id.action_bar_delete_menu_item) {
    184             Logger.get(activity).logImpression(DialerImpression.Type.MULTISELECT_TAP_DELETE_ICON);
    185             if (selectedItems.size() > 0) {
    186               showDeleteSelectedItemsDialog();
    187             }
    188             return true;
    189           } else {
    190             return false;
    191           }
    192         }
    193 
    194         // Called when the user exits the action mode
    195         @Override
    196         public void onDestroyActionMode(ActionMode mode) {
    197           if (activity != null) {
    198             announceforAccessibility(
    199                 activity.getCurrentFocus(),
    200                 activity.getString(R.string.description_leaving_bulk_action_mode));
    201           }
    202           selectedItems.clear();
    203           actionMode = null;
    204           selectAllMode = false;
    205           deselectAllMode = false;
    206           multiSelectRemoveView.showMultiSelectRemoveView(false);
    207           actionModeStateChangedListener.onActionModeStateChanged(false);
    208           notifyDataSetChanged();
    209         }
    210       };
    211 
    212   private void showDeleteSelectedItemsDialog() {
    213     SparseArray<String> voicemailsToDeleteOnConfirmation = selectedItems.clone();
    214     new AlertDialog.Builder(activity, R.style.AlertDialogCustom)
    215         .setCancelable(true)
    216         .setTitle(
    217             activity
    218                 .getResources()
    219                 .getQuantityString(
    220                     R.plurals.delete_voicemails_confirmation_dialog_title, selectedItems.size()))
    221         .setPositiveButton(
    222             R.string.voicemailMultiSelectDeleteConfirm,
    223             new DialogInterface.OnClickListener() {
    224               @Override
    225               public void onClick(final DialogInterface dialog, final int button) {
    226                 LogUtil.i(
    227                     "CallLogAdapter.showDeleteSelectedItemsDialog",
    228                     "onClick, these items to delete " + voicemailsToDeleteOnConfirmation);
    229                 deleteSelectedItems(voicemailsToDeleteOnConfirmation);
    230                 actionMode.finish();
    231                 dialog.cancel();
    232                 Logger.get(activity)
    233                     .logImpression(
    234                         DialerImpression.Type.MULTISELECT_DELETE_ENTRY_VIA_CONFIRMATION_DIALOG);
    235               }
    236             })
    237         .setOnCancelListener(
    238             new OnCancelListener() {
    239               @Override
    240               public void onCancel(DialogInterface dialogInterface) {
    241                 Logger.get(activity)
    242                     .logImpression(
    243                         DialerImpression.Type
    244                             .MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_TOUCH);
    245                 dialogInterface.cancel();
    246               }
    247             })
    248         .setNegativeButton(
    249             R.string.voicemailMultiSelectDeleteCancel,
    250             new DialogInterface.OnClickListener() {
    251               @Override
    252               public void onClick(final DialogInterface dialog, final int button) {
    253                 Logger.get(activity)
    254                     .logImpression(
    255                         DialerImpression.Type
    256                             .MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_BUTTON);
    257                 dialog.cancel();
    258               }
    259             })
    260         .show();
    261     Logger.get(activity)
    262         .logImpression(DialerImpression.Type.MULTISELECT_DISPLAY_DELETE_CONFIRMATION_DIALOG);
    263   }
    264 
    265   private void deleteSelectedItems(SparseArray<String> voicemailsToDelete) {
    266     for (int i = 0; i < voicemailsToDelete.size(); i++) {
    267       String voicemailUri = voicemailsToDelete.get(voicemailsToDelete.keyAt(i));
    268       LogUtil.i("CallLogAdapter.deleteSelectedItems", "deleting uri:" + voicemailUri);
    269       CallLogAsyncTaskUtil.deleteVoicemail(activity, Uri.parse(voicemailUri), null);
    270     }
    271   }
    272 
    273   private final View.OnLongClickListener longPressListener =
    274       new View.OnLongClickListener() {
    275         @Override
    276         public boolean onLongClick(View v) {
    277           if (ConfigProviderBindings.get(v.getContext())
    278                   .getBoolean(ENABLE_CALL_LOG_MULTI_SELECT, ENABLE_CALL_LOG_MULTI_SELECT_FLAG)
    279               && voicemailPlaybackPresenter != null) {
    280             if (v.getId() == R.id.primary_action_view || v.getId() == R.id.quick_contact_photo) {
    281               if (actionMode == null) {
    282                 Logger.get(activity)
    283                     .logImpression(
    284                         DialerImpression.Type.MULTISELECT_LONG_PRESS_ENTER_MULTI_SELECT_MODE);
    285                 actionMode = v.startActionMode(actionModeCallback);
    286               }
    287               Logger.get(activity)
    288                   .logImpression(DialerImpression.Type.MULTISELECT_LONG_PRESS_TAP_ENTRY);
    289               CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag();
    290               viewHolder.quickContactView.setVisibility(View.GONE);
    291               viewHolder.checkBoxView.setVisibility(View.VISIBLE);
    292               expandCollapseListener.onClick(v);
    293               return true;
    294             }
    295           }
    296           return true;
    297         }
    298       };
    299 
    300   @VisibleForTesting
    301   public View.OnClickListener getExpandCollapseListener() {
    302     return expandCollapseListener;
    303   }
    304 
    305   /** The OnClickListener used to expand or collapse the action buttons of a call log entry. */
    306   private final View.OnClickListener expandCollapseListener =
    307       new View.OnClickListener() {
    308         @Override
    309         public void onClick(View v) {
    310           PerformanceReport.recordClick(UiAction.Type.CLICK_CALL_LOG_ITEM);
    311 
    312           CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag();
    313           if (viewHolder == null) {
    314             return;
    315           }
    316           if (actionMode != null && viewHolder.voicemailUri != null) {
    317             selectAllMode = false;
    318             deselectAllMode = false;
    319             multiSelectRemoveView.setSelectAllModeToFalse();
    320             int id = getVoicemailId(viewHolder.voicemailUri);
    321             if (selectedItems.get(id) != null) {
    322               Logger.get(activity)
    323                   .logImpression(DialerImpression.Type.MULTISELECT_SINGLE_PRESS_UNSELECT_ENTRY);
    324               uncheckMarkCallLogEntry(viewHolder, id);
    325             } else {
    326               Logger.get(activity)
    327                   .logImpression(DialerImpression.Type.MULTISELECT_SINGLE_PRESS_SELECT_ENTRY);
    328               checkMarkCallLogEntry(viewHolder);
    329               // select all check box logic
    330               if (getItemCount() == selectedItems.size()) {
    331                 LogUtil.i(
    332                     "mExpandCollapseListener.onClick",
    333                     "getitem count %d is equal to items select count %d, check select all box",
    334                     getItemCount(),
    335                     selectedItems.size());
    336                 multiSelectRemoveView.tapSelectAll();
    337               }
    338             }
    339             return;
    340           }
    341 
    342           if (voicemailPlaybackPresenter != null) {
    343             // Always reset the voicemail playback state on expand or collapse.
    344             voicemailPlaybackPresenter.resetAll();
    345           }
    346 
    347           // If enriched call capabilities were unknown on the initial load,
    348           // viewHolder.isCallComposerCapable may be unset. Check here if we have the capabilities
    349           // as a last attempt at getting them before showing the expanded view to the user
    350           EnrichedCallCapabilities capabilities = null;
    351 
    352           if (viewHolder.number != null) {
    353             capabilities = getEnrichedCallManager().getCapabilities(viewHolder.number);
    354           }
    355 
    356           if (capabilities == null) {
    357             capabilities = EnrichedCallCapabilities.NO_CAPABILITIES;
    358           }
    359 
    360           viewHolder.isCallComposerCapable = capabilities.isCallComposerCapable();
    361 
    362           if (capabilities.isTemporarilyUnavailable()) {
    363             LogUtil.i(
    364                 "mExpandCollapseListener.onClick",
    365                 "%s is temporarily unavailable, requesting capabilities",
    366                 LogUtil.sanitizePhoneNumber(viewHolder.number));
    367             // Refresh the capabilities when temporarily unavailable.
    368             // Similarly to when we request capabilities the first time, the 'Share and call' button
    369             // won't pop in with the new capabilities. Instead the row needs to be collapsed and
    370             // expanded again.
    371             getEnrichedCallManager().requestCapabilities(viewHolder.number);
    372           }
    373 
    374           if (viewHolder.rowId == currentlyExpandedRowId) {
    375             // Hide actions, if the clicked item is the expanded item.
    376             viewHolder.showActions(false);
    377 
    378             currentlyExpandedPosition = RecyclerView.NO_POSITION;
    379             currentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
    380           } else {
    381             if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) {
    382               CallLogAsyncTaskUtil.markCallAsRead(activity, viewHolder.callIds);
    383               if (activityType == ACTIVITY_TYPE_DIALTACTS) {
    384                 if (v.getContext() instanceof MainActivityPeer.PeerSupplier) {
    385                   // This is really bad, but we must do this to prevent a dependency cycle, enforce
    386                   // best practices in new code, and avoid refactoring DialtactsActivity.
    387                   ((FragmentUtilListener)
    388                           ((MainActivityPeer.PeerSupplier) v.getContext()).getPeer())
    389                       .getImpl(CallLogFragmentListener.class)
    390                       .updateTabUnreadCounts();
    391                 } else {
    392                   ((DialtactsActivity) v.getContext()).updateTabUnreadCounts();
    393                 }
    394               }
    395             }
    396             expandViewHolderActions(viewHolder);
    397 
    398             if (isDuoCallButtonVisible(viewHolder.videoCallButtonView)) {
    399               CallIntentBuilder.increaseLightbringerCallButtonAppearInExpandedCallLogItemCount();
    400             }
    401           }
    402         }
    403 
    404         private boolean isDuoCallButtonVisible(View videoCallButtonView) {
    405           if (videoCallButtonView == null) {
    406             return false;
    407           }
    408           if (videoCallButtonView.getVisibility() != View.VISIBLE) {
    409             return false;
    410           }
    411           IntentProvider intentProvider = (IntentProvider) videoCallButtonView.getTag();
    412           if (intentProvider == null) {
    413             return false;
    414           }
    415           return DuoConstants.PACKAGE_NAME.equals(intentProvider.getIntent(activity).getPackage());
    416         }
    417       };
    418 
    419   @Nullable
    420   public RecyclerView.OnScrollListener getOnScrollListener() {
    421     return null;
    422   }
    423 
    424   private void checkMarkCallLogEntry(CallLogListItemViewHolder viewHolder) {
    425     announceforAccessibility(
    426         activity.getCurrentFocus(),
    427         activity.getString(
    428             R.string.description_selecting_bulk_action_mode, viewHolder.nameOrNumber));
    429     viewHolder.quickContactView.setVisibility(View.GONE);
    430     viewHolder.checkBoxView.setVisibility(View.VISIBLE);
    431     selectedItems.put(getVoicemailId(viewHolder.voicemailUri), viewHolder.voicemailUri);
    432     updateActionBar();
    433   }
    434 
    435   private void announceforAccessibility(View view, String announcement) {
    436     if (view != null) {
    437       view.announceForAccessibility(announcement);
    438     }
    439   }
    440 
    441   private void updateActionBar() {
    442     if (actionMode == null && selectedItems.size() > 0) {
    443       Logger.get(activity)
    444           .logImpression(DialerImpression.Type.MULTISELECT_ROTATE_AND_SHOW_ACTION_MODE);
    445       activity.startActionMode(actionModeCallback);
    446     }
    447     if (actionMode != null) {
    448       actionMode.setTitle(
    449           activity
    450               .getResources()
    451               .getString(
    452                   R.string.voicemailMultiSelectActionBarTitle,
    453                   Integer.toString(selectedItems.size())));
    454     }
    455   }
    456 
    457   private void uncheckMarkCallLogEntry(CallLogListItemViewHolder viewHolder, int id) {
    458     announceforAccessibility(
    459         activity.getCurrentFocus(),
    460         activity.getString(
    461             R.string.description_unselecting_bulk_action_mode, viewHolder.nameOrNumber));
    462     selectedItems.delete(id);
    463     viewHolder.checkBoxView.setVisibility(View.GONE);
    464     viewHolder.quickContactView.setVisibility(View.VISIBLE);
    465     updateActionBar();
    466   }
    467 
    468   private static int getVoicemailId(String voicemailUri) {
    469     Assert.checkArgument(voicemailUri != null);
    470     Assert.checkArgument(voicemailUri.length() > 0);
    471     return (int) ContentUris.parseId(Uri.parse(voicemailUri));
    472   }
    473 
    474   /**
    475    * A list of {@link CallLogQuery#ID} that will be hidden. The hide might be temporary so instead
    476    * if removing an item, it will be shown as an invisible view. This simplifies the calculation of
    477    * item position.
    478    */
    479   @NonNull private Set<Long> hiddenRowIds = new ArraySet<>();
    480   /**
    481    * Holds a list of URIs that are pending deletion or undo. If the activity ends before the undo
    482    * timeout, all of the pending URIs will be deleted.
    483    *
    484    * <p>TODO(twyen): move this and OnVoicemailDeletedListener to somewhere like {@link
    485    * VisualVoicemailCallLogFragment}. The CallLogAdapter does not need to know about what to do with
    486    * hidden item or what to hide.
    487    */
    488   @NonNull private final Set<Uri> hiddenItemUris = new ArraySet<>();
    489 
    490   private CallLogListItemViewHolder.OnClickListener blockReportSpamListener;
    491 
    492   /**
    493    * Map, keyed by call ID, used to track the callback action for a call. Calls associated with the
    494    * same callback action will be put into the same primary call group in {@link
    495    * com.android.dialer.app.calllog.CallLogGroupBuilder}. This information is used to set the
    496    * callback icon and trigger the corresponding action.
    497    */
    498   private final Map<Long, Integer> callbackActions = new ArrayMap<>();
    499 
    500   /**
    501    * Map, keyed by call ID, used to track the day group for a call. As call log entries are put into
    502    * the primary call groups in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}, they are
    503    * also assigned a secondary "day group". This map tracks the day group assigned to all calls in
    504    * the call log. This information is used to trigger the display of a day group header above the
    505    * call log entry at the start of a day group. Note: Multiple calls are grouped into a single
    506    * primary "call group" in the call log, and the cursor used to bind rows includes all of these
    507    * calls. When determining if a day group change has occurred it is necessary to look at the last
    508    * entry in the call log to determine its day group. This map provides a means of determining the
    509    * previous day group without having to reverse the cursor to the start of the previous day call
    510    * log entry.
    511    */
    512   private final Map<Long, Integer> dayGroups = new ArrayMap<>();
    513 
    514   private boolean loading = true;
    515   private ContactsPreferences contactsPreferences;
    516 
    517   private boolean isSpamEnabled;
    518 
    519   public CallLogAdapter(
    520       Activity activity,
    521       ViewGroup alertContainer,
    522       CallFetcher callFetcher,
    523       MultiSelectRemoveView multiSelectRemoveView,
    524       OnActionModeStateChangedListener actionModeStateChangedListener,
    525       CallLogCache callLogCache,
    526       ContactInfoCache contactInfoCache,
    527       VoicemailPlaybackPresenter voicemailPlaybackPresenter,
    528       @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
    529       int activityType) {
    530     super();
    531 
    532     this.activity = activity;
    533     this.callFetcher = callFetcher;
    534     this.actionModeStateChangedListener = actionModeStateChangedListener;
    535     this.multiSelectRemoveView = multiSelectRemoveView;
    536     this.voicemailPlaybackPresenter = voicemailPlaybackPresenter;
    537     if (this.voicemailPlaybackPresenter != null) {
    538       this.voicemailPlaybackPresenter.setOnVoicemailDeletedListener(this);
    539     }
    540 
    541     this.activityType = activityType;
    542 
    543     this.contactInfoCache = contactInfoCache;
    544 
    545     if (!PermissionsUtil.hasContactsReadPermissions(activity)) {
    546       this.contactInfoCache.disableRequestProcessing();
    547     }
    548 
    549     Resources resources = this.activity.getResources();
    550 
    551     this.callLogCache = callLogCache;
    552 
    553     PhoneCallDetailsHelper phoneCallDetailsHelper =
    554         new PhoneCallDetailsHelper(this.activity, resources, this.callLogCache);
    555     callLogListItemHelper =
    556         new CallLogListItemHelper(phoneCallDetailsHelper, resources, this.callLogCache);
    557     callLogGroupBuilder = new CallLogGroupBuilder(this);
    558     this.filteredNumberAsyncQueryHandler = Assert.isNotNull(filteredNumberAsyncQueryHandler);
    559 
    560     contactsPreferences = new ContactsPreferences(this.activity);
    561 
    562     blockReportSpamListener =
    563         new BlockReportSpamListener(
    564             this.activity,
    565             ((Activity) this.activity).getFragmentManager(),
    566             this,
    567             this.filteredNumberAsyncQueryHandler);
    568     setHasStableIds(true);
    569 
    570     callLogAlertManager =
    571         new CallLogAlertManager(this, LayoutInflater.from(this.activity), alertContainer);
    572   }
    573 
    574   private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
    575     if (!TextUtils.isEmpty(viewHolder.voicemailUri)) {
    576       Logger.get(activity).logImpression(DialerImpression.Type.VOICEMAIL_EXPAND_ENTRY);
    577     }
    578 
    579     int lastExpandedPosition = currentlyExpandedPosition;
    580     // Show the actions for the clicked list item.
    581     viewHolder.showActions(true);
    582     currentlyExpandedPosition = viewHolder.getAdapterPosition();
    583     currentlyExpandedRowId = viewHolder.rowId;
    584 
    585     // If another item is expanded, notify it that it has changed. Its actions will be
    586     // hidden when it is re-binded because we change mCurrentlyExpandedRowId above.
    587     if (lastExpandedPosition != RecyclerView.NO_POSITION) {
    588       notifyItemChanged(lastExpandedPosition);
    589     }
    590   }
    591 
    592   public void onSaveInstanceState(Bundle outState) {
    593     outState.putInt(KEY_EXPANDED_POSITION, currentlyExpandedPosition);
    594     outState.putLong(KEY_EXPANDED_ROW_ID, currentlyExpandedRowId);
    595 
    596     ArrayList<String> listOfSelectedItems = new ArrayList<>();
    597 
    598     if (selectedItems.size() > 0) {
    599       for (int i = 0; i < selectedItems.size(); i++) {
    600         int id = selectedItems.keyAt(i);
    601         String voicemailUri = selectedItems.valueAt(i);
    602         LogUtil.i(
    603             "CallLogAdapter.onSaveInstanceState", "index %d, id=%d, uri=%s ", i, id, voicemailUri);
    604         listOfSelectedItems.add(voicemailUri);
    605       }
    606     }
    607     outState.putStringArrayList(KEY_ACTION_MODE, listOfSelectedItems);
    608 
    609     LogUtil.i(
    610         "CallLogAdapter.onSaveInstanceState",
    611         "saved: %d, selectedItemsSize:%d",
    612         listOfSelectedItems.size(),
    613         selectedItems.size());
    614   }
    615 
    616   public void onRestoreInstanceState(Bundle savedInstanceState) {
    617     if (savedInstanceState != null) {
    618       currentlyExpandedPosition =
    619           savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION);
    620       currentlyExpandedRowId =
    621           savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM);
    622       // Restoring multi selected entries
    623       ArrayList<String> listOfSelectedItems =
    624           savedInstanceState.getStringArrayList(KEY_ACTION_MODE);
    625       if (listOfSelectedItems != null) {
    626         LogUtil.i(
    627             "CallLogAdapter.onRestoreInstanceState",
    628             "restored selectedItemsList:%d",
    629             listOfSelectedItems.size());
    630 
    631         if (!listOfSelectedItems.isEmpty()) {
    632           for (int i = 0; i < listOfSelectedItems.size(); i++) {
    633             String voicemailUri = listOfSelectedItems.get(i);
    634             int id = getVoicemailId(voicemailUri);
    635             LogUtil.i(
    636                 "CallLogAdapter.onRestoreInstanceState",
    637                 "restoring selected index %d, id=%d, uri=%s ",
    638                 i,
    639                 id,
    640                 voicemailUri);
    641             selectedItems.put(id, voicemailUri);
    642           }
    643 
    644           LogUtil.i(
    645               "CallLogAdapter.onRestoreInstance",
    646               "restored selectedItems %s",
    647               selectedItems.toString());
    648           updateActionBar();
    649         }
    650       }
    651     }
    652   }
    653 
    654   /** Requery on background thread when {@link Cursor} changes. */
    655   @Override
    656   protected void onContentChanged() {
    657     callFetcher.fetchCalls();
    658   }
    659 
    660   public void setLoading(boolean loading) {
    661     this.loading = loading;
    662   }
    663 
    664   public boolean isEmpty() {
    665     if (loading) {
    666       // We don't want the empty state to show when loading.
    667       return false;
    668     } else {
    669       return getItemCount() == 0;
    670     }
    671   }
    672 
    673   public void clearFilteredNumbersCache() {
    674     filteredNumberAsyncQueryHandler.clearCache();
    675   }
    676 
    677   public void onResume() {
    678     if (PermissionsUtil.hasPermission(activity, android.Manifest.permission.READ_CONTACTS)) {
    679       contactInfoCache.start();
    680     }
    681     contactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
    682     isSpamEnabled = SpamComponent.get(activity).spam().isSpamEnabled();
    683     getDuo().registerListener(this);
    684     notifyDataSetChanged();
    685   }
    686 
    687   public void onPause() {
    688     getDuo().unregisterListener(this);
    689     pauseCache();
    690     for (Uri uri : hiddenItemUris) {
    691       CallLogAsyncTaskUtil.deleteVoicemail(activity, uri, null);
    692     }
    693   }
    694 
    695   public void onStop() {
    696     getEnrichedCallManager().clearCachedData();
    697   }
    698 
    699   public CallLogAlertManager getAlertManager() {
    700     return callLogAlertManager;
    701   }
    702 
    703   @VisibleForTesting
    704   /* package */ void pauseCache() {
    705     contactInfoCache.stop();
    706     callLogCache.reset();
    707   }
    708 
    709   @Override
    710   protected void addGroups(Cursor cursor) {
    711     callLogGroupBuilder.addGroups(cursor);
    712   }
    713 
    714   @Override
    715   public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    716     if (viewType == VIEW_TYPE_ALERT) {
    717       return callLogAlertManager.createViewHolder(parent);
    718     }
    719     return createCallLogEntryViewHolder(parent);
    720   }
    721 
    722   /**
    723    * Creates a new call log entry {@link ViewHolder}.
    724    *
    725    * @param parent the parent view.
    726    * @return The {@link ViewHolder}.
    727    */
    728   private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) {
    729     LayoutInflater inflater = LayoutInflater.from(activity);
    730     View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
    731     CallLogListItemViewHolder viewHolder =
    732         CallLogListItemViewHolder.create(
    733             view,
    734             activity,
    735             blockReportSpamListener,
    736             expandCollapseListener,
    737             longPressListener,
    738             actionModeStateChangedListener,
    739             callLogCache,
    740             callLogListItemHelper,
    741             voicemailPlaybackPresenter);
    742 
    743     viewHolder.callLogEntryView.setTag(viewHolder);
    744 
    745     viewHolder.primaryActionView.setTag(viewHolder);
    746     viewHolder.quickContactView.setTag(viewHolder);
    747 
    748     return viewHolder;
    749   }
    750 
    751   /**
    752    * Binds the views in the entry to the data in the call log. TODO: This gets called 20-30 times
    753    * when Dialer starts up for a single call log entry and should not. It invokes cross-process
    754    * methods and the repeat execution can get costly.
    755    *
    756    * @param viewHolder The view corresponding to this entry.
    757    * @param position The position of the entry.
    758    */
    759   @Override
    760   public void onBindViewHolder(ViewHolder viewHolder, int position) {
    761     Trace.beginSection("onBindViewHolder: " + position);
    762     switch (getItemViewType(position)) {
    763       case VIEW_TYPE_ALERT:
    764         // Do nothing
    765         break;
    766       default:
    767         bindCallLogListViewHolder(viewHolder, position);
    768         break;
    769     }
    770     Trace.endSection();
    771   }
    772 
    773   @Override
    774   public void onViewRecycled(ViewHolder viewHolder) {
    775     if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
    776       CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
    777       updateCheckMarkedStatusOfEntry(views);
    778 
    779       if (views.asyncTask != null) {
    780         views.asyncTask.cancel(true);
    781       }
    782     }
    783   }
    784 
    785   @Override
    786   public void onViewAttachedToWindow(ViewHolder viewHolder) {
    787     if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
    788       ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = true;
    789     }
    790   }
    791 
    792   @Override
    793   public void onViewDetachedFromWindow(ViewHolder viewHolder) {
    794     if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
    795       ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = false;
    796     }
    797   }
    798 
    799   /**
    800    * Binds the view holder for the call log list item view.
    801    *
    802    * @param viewHolder The call log list item view holder.
    803    * @param position The position of the list item.
    804    */
    805   private void bindCallLogListViewHolder(final ViewHolder viewHolder, final int position) {
    806     Cursor c = (Cursor) getItem(position);
    807     if (c == null) {
    808       return;
    809     }
    810     CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
    811     updateCheckMarkedStatusOfEntry(views);
    812 
    813     views.isLoaded = false;
    814     int groupSize = getGroupSize(position);
    815     CallDetailsEntries callDetailsEntries = createCallDetailsEntries(c, groupSize);
    816     PhoneCallDetails details = createPhoneCallDetails(c, groupSize, views);
    817     if (isHiddenRow(views.number, c.getLong(CallLogQuery.ID))) {
    818       views.callLogEntryView.setVisibility(View.GONE);
    819       views.dayGroupHeader.setVisibility(View.GONE);
    820       return;
    821     } else {
    822       views.callLogEntryView.setVisibility(View.VISIBLE);
    823       // dayGroupHeader will be restored after loadAndRender() if it is needed.
    824     }
    825     if (currentlyExpandedRowId == views.rowId) {
    826       views.inflateActionViewStub();
    827     }
    828     loadAndRender(views, views.rowId, details, callDetailsEntries);
    829   }
    830 
    831   private void updateCheckMarkedStatusOfEntry(CallLogListItemViewHolder views) {
    832     if (selectedItems.size() > 0 && views.voicemailUri != null) {
    833       int id = getVoicemailId(views.voicemailUri);
    834       if (selectedItems.get(id) != null) {
    835         checkMarkCallLogEntry(views);
    836       } else {
    837         uncheckMarkCallLogEntry(views, id);
    838       }
    839     }
    840   }
    841 
    842   private boolean isHiddenRow(@Nullable String number, long rowId) {
    843     if (number != null && PhoneNumberUtils.isEmergencyNumber(number)) {
    844       return true;
    845     }
    846     if (hiddenRowIds.contains(rowId)) {
    847       return true;
    848     }
    849     return false;
    850   }
    851 
    852   private void loadAndRender(
    853       final CallLogListItemViewHolder viewHolder,
    854       final long rowId,
    855       final PhoneCallDetails details,
    856       final CallDetailsEntries callDetailsEntries) {
    857     LogUtil.d("CallLogAdapter.loadAndRender", "position: %d", viewHolder.getAdapterPosition());
    858     // Reset block and spam information since this view could be reused which may contain
    859     // outdated data.
    860     viewHolder.isSpam = false;
    861     viewHolder.blockId = null;
    862     viewHolder.isSpamFeatureEnabled = false;
    863 
    864     // Attempt to set the isCallComposerCapable field. If capabilities are unknown for this number,
    865     // the value will be false while capabilities are requested. mExpandCollapseListener will
    866     // attempt to set the field properly in that case
    867     viewHolder.isCallComposerCapable = isCallComposerCapable(viewHolder.number);
    868     viewHolder.setDetailedPhoneDetails(callDetailsEntries);
    869     final AsyncTask<Void, Void, Boolean> loadDataTask =
    870         new AsyncTask<Void, Void, Boolean>() {
    871           @Override
    872           protected Boolean doInBackground(Void... params) {
    873             viewHolder.blockId =
    874                 filteredNumberAsyncQueryHandler.getBlockedIdSynchronous(
    875                     viewHolder.number, viewHolder.countryIso);
    876             details.isBlocked = viewHolder.blockId != null;
    877             if (isCancelled()) {
    878               return false;
    879             }
    880             if (isSpamEnabled) {
    881               viewHolder.isSpamFeatureEnabled = true;
    882               // Only display the call as a spam call if there are incoming calls in the list.
    883               // Call log cards with only outgoing calls should never be displayed as spam.
    884               viewHolder.isSpam =
    885                   details.hasIncomingCalls()
    886                       && SpamComponent.get(activity)
    887                           .spam()
    888                           .checkSpamStatusSynchronous(viewHolder.number, viewHolder.countryIso);
    889               details.isSpam = viewHolder.isSpam;
    890             }
    891             return !isCancelled() && loadData(viewHolder, rowId, details);
    892           }
    893 
    894           @Override
    895           protected void onPostExecute(Boolean success) {
    896             viewHolder.isLoaded = true;
    897             if (success) {
    898               viewHolder.callbackAction = getCallbackAction(viewHolder.rowId);
    899               int currentDayGroup = getDayGroup(viewHolder.rowId);
    900               if (currentDayGroup != details.previousGroup) {
    901                 viewHolder.dayGroupHeaderVisibility = View.VISIBLE;
    902                 viewHolder.dayGroupHeaderText = getGroupDescription(currentDayGroup);
    903               } else {
    904                 viewHolder.dayGroupHeaderVisibility = View.GONE;
    905               }
    906               render(viewHolder, details, rowId);
    907             }
    908           }
    909         };
    910 
    911     viewHolder.asyncTask = loadDataTask;
    912     asyncTaskExecutor.submit(LOAD_DATA_TASK_IDENTIFIER, loadDataTask);
    913   }
    914 
    915   @MainThread
    916   private boolean isCallComposerCapable(@Nullable String number) {
    917     if (number == null) {
    918       return false;
    919     }
    920 
    921     EnrichedCallCapabilities capabilities = getEnrichedCallManager().getCapabilities(number);
    922     if (capabilities == null) {
    923       getEnrichedCallManager().requestCapabilities(number);
    924       return false;
    925     }
    926     return capabilities.isCallComposerCapable();
    927   }
    928 
    929   /**
    930    * Initialize PhoneCallDetails by reading all data from cursor. This method must be run on main
    931    * thread since cursor is not thread safe.
    932    */
    933   @MainThread
    934   private PhoneCallDetails createPhoneCallDetails(
    935       Cursor cursor, int count, final CallLogListItemViewHolder views) {
    936     Assert.isMainThread();
    937     final String number = cursor.getString(CallLogQuery.NUMBER);
    938     final String postDialDigits =
    939         (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
    940     final String viaNumber =
    941         (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : "";
    942     final int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION);
    943     final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(cursor);
    944     final int transcriptionState =
    945         (VERSION.SDK_INT >= VERSION_CODES.O)
    946             ? cursor.getInt(CallLogQuery.TRANSCRIPTION_STATE)
    947             : VoicemailCompat.TRANSCRIPTION_NOT_STARTED;
    948     final PhoneCallDetails details =
    949         new PhoneCallDetails(number, numberPresentation, postDialDigits);
    950     details.viaNumber = viaNumber;
    951     details.countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
    952     details.date = cursor.getLong(CallLogQuery.DATE);
    953     details.duration = cursor.getLong(CallLogQuery.DURATION);
    954     details.features = getCallFeatures(cursor, count);
    955     details.geocode = cursor.getString(CallLogQuery.GEOCODED_LOCATION);
    956     details.transcription = cursor.getString(CallLogQuery.TRANSCRIPTION);
    957     details.transcriptionState = transcriptionState;
    958     details.callTypes = getCallTypes(cursor, count);
    959 
    960     details.accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
    961     details.accountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
    962     details.cachedContactInfo = cachedContactInfo;
    963 
    964     if (!cursor.isNull(CallLogQuery.DATA_USAGE)) {
    965       details.dataUsage = cursor.getLong(CallLogQuery.DATA_USAGE);
    966     }
    967 
    968     views.rowId = cursor.getLong(CallLogQuery.ID);
    969     // Stash away the Ids of the calls so that we can support deleting a row in the call log.
    970     views.callIds = getCallIds(cursor, count);
    971     details.previousGroup = getPreviousDayGroup(cursor);
    972 
    973     // Store values used when the actions ViewStub is inflated on expansion.
    974     views.number = number;
    975     views.countryIso = details.countryIso;
    976     views.postDialDigits = details.postDialDigits;
    977     views.numberPresentation = numberPresentation;
    978 
    979     if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE
    980         || details.callTypes[0] == CallLog.Calls.MISSED_TYPE) {
    981       details.isRead = cursor.getInt(CallLogQuery.IS_READ) == 1;
    982     }
    983     views.callType = cursor.getInt(CallLogQuery.CALL_TYPE);
    984     views.voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI);
    985     details.voicemailUri = views.voicemailUri;
    986 
    987     return details;
    988   }
    989 
    990   @MainThread
    991   private CallDetailsEntries createCallDetailsEntries(Cursor cursor, int count) {
    992     Assert.isMainThread();
    993     int position = cursor.getPosition();
    994     CallDetailsEntries.Builder entries = CallDetailsEntries.newBuilder();
    995     for (int i = 0; i < count; i++) {
    996       CallDetailsEntry.Builder entry =
    997           CallDetailsEntry.newBuilder()
    998               .setCallId(cursor.getLong(CallLogQuery.ID))
    999               .setCallType(cursor.getInt(CallLogQuery.CALL_TYPE))
   1000               .setDataUsage(cursor.getLong(CallLogQuery.DATA_USAGE))
   1001               .setDate(cursor.getLong(CallLogQuery.DATE))
   1002               .setDuration(cursor.getLong(CallLogQuery.DURATION))
   1003               .setFeatures(cursor.getInt(CallLogQuery.FEATURES));
   1004 
   1005       String phoneAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
   1006       if (DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME
   1007           .flattenToString()
   1008           .equals(phoneAccountComponentName)) {
   1009         entry.setIsDuoCall(true);
   1010       }
   1011 
   1012       entries.addEntries(entry.build());
   1013       cursor.moveToNext();
   1014     }
   1015     cursor.moveToPosition(position);
   1016     return entries.build();
   1017   }
   1018 
   1019   /**
   1020    * Load data for call log. Any expensive operation should be put here to avoid blocking main
   1021    * thread. Do NOT put any cursor operation here since it's not thread safe.
   1022    */
   1023   @WorkerThread
   1024   private boolean loadData(CallLogListItemViewHolder views, long rowId, PhoneCallDetails details) {
   1025     Assert.isWorkerThread();
   1026     if (rowId != views.rowId) {
   1027       LogUtil.i(
   1028           "CallLogAdapter.loadData",
   1029           "rowId of viewHolder changed after load task is issued, aborting load");
   1030       return false;
   1031     }
   1032 
   1033     final PhoneAccountHandle accountHandle =
   1034         TelecomUtil.composePhoneAccountHandle(details.accountComponentName, details.accountId);
   1035 
   1036     final boolean isVoicemailNumber = callLogCache.isVoicemailNumber(accountHandle, details.number);
   1037 
   1038     // Note: Binding of the action buttons is done as required in configureActionViews when the
   1039     // user expands the actions ViewStub.
   1040 
   1041     ContactInfo info = ContactInfo.EMPTY;
   1042     if (PhoneNumberHelper.canPlaceCallsTo(details.number, details.numberPresentation)
   1043         && !isVoicemailNumber) {
   1044       // Lookup contacts with this number
   1045       // Only do remote lookup in first 5 rows.
   1046       int position = views.getAdapterPosition();
   1047       info =
   1048           contactInfoCache.getValue(
   1049               details.number + details.postDialDigits,
   1050               details.countryIso,
   1051               details.cachedContactInfo,
   1052               position
   1053                   < ConfigProviderBindings.get(activity)
   1054                       .getLong("number_of_call_to_do_remote_lookup", 5L));
   1055     }
   1056     CharSequence formattedNumber =
   1057         info.formattedNumber == null
   1058             ? null
   1059             : PhoneNumberUtils.createTtsSpannable(info.formattedNumber);
   1060     details.updateDisplayNumber(activity, formattedNumber, isVoicemailNumber);
   1061 
   1062     views.displayNumber = details.displayNumber;
   1063     views.accountHandle = accountHandle;
   1064     details.accountHandle = accountHandle;
   1065 
   1066     if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) {
   1067       details.contactUri = info.lookupUri;
   1068       details.namePrimary = info.name;
   1069       details.nameAlternative = info.nameAlternative;
   1070       details.nameDisplayOrder = contactsPreferences.getDisplayOrder();
   1071       details.numberType = info.type;
   1072       details.numberLabel = info.label;
   1073       details.photoUri = info.photoUri;
   1074       details.sourceType = info.sourceType;
   1075       details.objectId = info.objectId;
   1076       details.contactUserType = info.userType;
   1077     }
   1078     LogUtil.d(
   1079         "CallLogAdapter.loadData",
   1080         "position:%d, update geo info: %s, cequint caller id geo: %s, photo uri: %s <- %s",
   1081         views.getAdapterPosition(),
   1082         details.geocode,
   1083         info.geoDescription,
   1084         details.photoUri,
   1085         info.photoUri);
   1086     if (!TextUtils.isEmpty(info.geoDescription)) {
   1087       details.geocode = info.geoDescription;
   1088     }
   1089 
   1090     views.info = info;
   1091     views.numberType = getNumberType(activity.getResources(), details);
   1092 
   1093     callLogListItemHelper.updatePhoneCallDetails(details);
   1094     return true;
   1095   }
   1096 
   1097   private static String getNumberType(Resources res, PhoneCallDetails details) {
   1098     // Label doesn't make much sense if the information is coming from CNAP or Cequint Caller ID.
   1099     if (details.sourceType == ContactSource.Type.SOURCE_TYPE_CNAP
   1100         || details.sourceType == ContactSource.Type.SOURCE_TYPE_CEQUINT_CALLER_ID) {
   1101       return "";
   1102     }
   1103     // Returns empty label instead of "custom" if the custom label is empty.
   1104     if (details.numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(details.numberLabel)) {
   1105       return "";
   1106     }
   1107     return (String) Phone.getTypeLabel(res, details.numberType, details.numberLabel);
   1108   }
   1109 
   1110   /**
   1111    * Render item view given position. This is running on UI thread so DO NOT put any expensive
   1112    * operation into it.
   1113    */
   1114   @MainThread
   1115   private void render(CallLogListItemViewHolder views, PhoneCallDetails details, long rowId) {
   1116     Assert.isMainThread();
   1117     if (rowId != views.rowId) {
   1118       LogUtil.i(
   1119           "CallLogAdapter.render",
   1120           "rowId of viewHolder changed after load task is issued, aborting render");
   1121       return;
   1122     }
   1123 
   1124     // Default case: an item in the call log.
   1125     views.primaryActionView.setVisibility(View.VISIBLE);
   1126     views.workIconView.setVisibility(
   1127         details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE);
   1128 
   1129     if (selectAllMode && views.voicemailUri != null) {
   1130       selectedItems.put(getVoicemailId(views.voicemailUri), views.voicemailUri);
   1131     }
   1132     if (deselectAllMode && views.voicemailUri != null) {
   1133       selectedItems.delete(getVoicemailId(views.voicemailUri));
   1134     }
   1135     if (views.voicemailUri != null
   1136         && selectedItems.get(getVoicemailId(views.voicemailUri)) != null) {
   1137       views.checkBoxView.setVisibility(View.VISIBLE);
   1138       views.quickContactView.setVisibility(View.GONE);
   1139     } else if (views.voicemailUri != null) {
   1140       views.checkBoxView.setVisibility(View.GONE);
   1141       views.quickContactView.setVisibility(View.VISIBLE);
   1142     }
   1143     callLogListItemHelper.setPhoneCallDetails(views, details);
   1144     if (currentlyExpandedRowId == views.rowId) {
   1145       // In case ViewHolders were added/removed, update the expanded position if the rowIds
   1146       // match so that we can restore the correct expanded state on rebind.
   1147       currentlyExpandedPosition = views.getAdapterPosition();
   1148       views.showActions(true);
   1149     } else {
   1150       views.showActions(false);
   1151     }
   1152     views.dayGroupHeader.setVisibility(views.dayGroupHeaderVisibility);
   1153     views.dayGroupHeader.setText(views.dayGroupHeaderText);
   1154   }
   1155 
   1156   @Override
   1157   public int getItemCount() {
   1158     return super.getItemCount() + (callLogAlertManager.isEmpty() ? 0 : 1);
   1159   }
   1160 
   1161   @Override
   1162   public int getItemViewType(int position) {
   1163     if (position == ALERT_POSITION && !callLogAlertManager.isEmpty()) {
   1164       return VIEW_TYPE_ALERT;
   1165     }
   1166     return VIEW_TYPE_CALLLOG;
   1167   }
   1168 
   1169   /**
   1170    * Retrieves an item at the specified position, taking into account the presence of a promo card.
   1171    *
   1172    * @param position The position to retrieve.
   1173    * @return The item at that position.
   1174    */
   1175   @Override
   1176   public Object getItem(int position) {
   1177     return super.getItem(position - (callLogAlertManager.isEmpty() ? 0 : 1));
   1178   }
   1179 
   1180   @Override
   1181   public long getItemId(int position) {
   1182     Cursor cursor = (Cursor) getItem(position);
   1183     if (cursor != null) {
   1184       return cursor.getLong(CallLogQuery.ID);
   1185     } else {
   1186       return 0;
   1187     }
   1188   }
   1189 
   1190   @Override
   1191   public int getGroupSize(int position) {
   1192     return super.getGroupSize(position - (callLogAlertManager.isEmpty() ? 0 : 1));
   1193   }
   1194 
   1195   protected boolean isCallLogActivity() {
   1196     return activityType == ACTIVITY_TYPE_CALL_LOG;
   1197   }
   1198 
   1199   /**
   1200    * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user
   1201    * clicks the delete button, the deleted item is temporarily hidden from the list. If a user
   1202    * clicks delete on a second item before the first item's undo option has expired, the first item
   1203    * is immediately deleted so that only one item can be "undoed" at a time.
   1204    */
   1205   @Override
   1206   public void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri) {
   1207     hiddenRowIds.add(viewHolder.rowId);
   1208     // Save the new hidden item uri in case the activity is suspend before the undo has timed out.
   1209     hiddenItemUris.add(uri);
   1210 
   1211     collapseExpandedCard();
   1212     notifyItemChanged(viewHolder.getAdapterPosition());
   1213     // The next item might have to update its day group label
   1214     notifyItemChanged(viewHolder.getAdapterPosition() + 1);
   1215   }
   1216 
   1217   private void collapseExpandedCard() {
   1218     currentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
   1219     currentlyExpandedPosition = RecyclerView.NO_POSITION;
   1220   }
   1221 
   1222   /** When the list is changing all stored position is no longer valid. */
   1223   public void invalidatePositions() {
   1224     currentlyExpandedPosition = RecyclerView.NO_POSITION;
   1225   }
   1226 
   1227   /** When the user clicks "undo", the hidden item is unhidden. */
   1228   @Override
   1229   public void onVoicemailDeleteUndo(long rowId, int adapterPosition, Uri uri) {
   1230     hiddenItemUris.remove(uri);
   1231     hiddenRowIds.remove(rowId);
   1232     notifyItemChanged(adapterPosition);
   1233     // The next item might have to update its day group label
   1234     notifyItemChanged(adapterPosition + 1);
   1235   }
   1236 
   1237   /** This callback signifies that a database deletion has completed. */
   1238   @Override
   1239   public void onVoicemailDeletedInDatabase(long rowId, Uri uri) {
   1240     hiddenItemUris.remove(uri);
   1241   }
   1242 
   1243   /**
   1244    * Retrieves the day group of the previous call in the call log. Used to determine if the day
   1245    * group has changed and to trigger display of the day group text.
   1246    *
   1247    * @param cursor The call log cursor.
   1248    * @return The previous day group, or DAY_GROUP_NONE if this is the first call.
   1249    */
   1250   private int getPreviousDayGroup(Cursor cursor) {
   1251     // We want to restore the position in the cursor at the end.
   1252     int startingPosition = cursor.getPosition();
   1253     moveToPreviousNonHiddenRow(cursor);
   1254     if (cursor.isBeforeFirst()) {
   1255       cursor.moveToPosition(startingPosition);
   1256       return CallLogGroupBuilder.DAY_GROUP_NONE;
   1257     }
   1258     int result = getDayGroup(cursor.getLong(CallLogQuery.ID));
   1259     cursor.moveToPosition(startingPosition);
   1260     return result;
   1261   }
   1262 
   1263   private void moveToPreviousNonHiddenRow(Cursor cursor) {
   1264     while (cursor.moveToPrevious() && hiddenRowIds.contains(cursor.getLong(CallLogQuery.ID))) {}
   1265   }
   1266 
   1267   /**
   1268    * Given a call ID, look up its callback action. Callback action data are populated in {@link
   1269    * com.android.dialer.app.calllog.CallLogGroupBuilder}.
   1270    *
   1271    * @param callId The call ID to retrieve the callback action.
   1272    * @return The callback action for the call.
   1273    */
   1274   @MainThread
   1275   private int getCallbackAction(long callId) {
   1276     Integer result = callbackActions.get(callId);
   1277     if (result != null) {
   1278       return result;
   1279     }
   1280     return CallbackAction.NONE;
   1281   }
   1282 
   1283   /**
   1284    * Given a call ID, look up the day group the call belongs to. Day group data are populated in
   1285    * {@link com.android.dialer.app.calllog.CallLogGroupBuilder}.
   1286    *
   1287    * @param callId The call ID to retrieve the day group.
   1288    * @return The day group for the call.
   1289    */
   1290   @MainThread
   1291   private int getDayGroup(long callId) {
   1292     Integer result = dayGroups.get(callId);
   1293     if (result != null) {
   1294       return result;
   1295     }
   1296     return CallLogGroupBuilder.DAY_GROUP_NONE;
   1297   }
   1298 
   1299   /**
   1300    * Returns the call types for the given number of items in the cursor.
   1301    *
   1302    * <p>It uses the next {@code count} rows in the cursor to extract the types.
   1303    *
   1304    * <p>It position in the cursor is unchanged by this function.
   1305    */
   1306   private static int[] getCallTypes(Cursor cursor, int count) {
   1307     int position = cursor.getPosition();
   1308     int[] callTypes = new int[count];
   1309     for (int index = 0; index < count; ++index) {
   1310       callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
   1311       cursor.moveToNext();
   1312     }
   1313     cursor.moveToPosition(position);
   1314     return callTypes;
   1315   }
   1316 
   1317   /**
   1318    * Determine the features which were enabled for any of the calls that make up a call log entry.
   1319    *
   1320    * @param cursor The cursor.
   1321    * @param count The number of calls for the current call log entry.
   1322    * @return The features.
   1323    */
   1324   private int getCallFeatures(Cursor cursor, int count) {
   1325     int features = 0;
   1326     int position = cursor.getPosition();
   1327     for (int index = 0; index < count; ++index) {
   1328       features |= cursor.getInt(CallLogQuery.FEATURES);
   1329       cursor.moveToNext();
   1330     }
   1331     cursor.moveToPosition(position);
   1332     return features;
   1333   }
   1334 
   1335   /**
   1336    * Sets whether processing of requests for contact details should be enabled.
   1337    *
   1338    * <p>This method should be called in tests to disable such processing of requests when not
   1339    * needed.
   1340    */
   1341   @VisibleForTesting
   1342   void disableRequestProcessingForTest() {
   1343     // TODO: Remove this and test the cache directly.
   1344     contactInfoCache.disableRequestProcessing();
   1345   }
   1346 
   1347   @VisibleForTesting
   1348   void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
   1349     // TODO: Remove this and test the cache directly.
   1350     contactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo);
   1351   }
   1352 
   1353   /**
   1354    * Stores the callback action associated with a call in the call log.
   1355    *
   1356    * @param rowId The row ID of the current call.
   1357    * @param callbackAction The current call's callback action.
   1358    */
   1359   @Override
   1360   @MainThread
   1361   public void setCallbackAction(long rowId, @CallbackAction int callbackAction) {
   1362     callbackActions.put(rowId, callbackAction);
   1363   }
   1364 
   1365   /**
   1366    * Stores the day group associated with a call in the call log.
   1367    *
   1368    * @param rowId The row ID of the current call.
   1369    * @param dayGroup The day group the call belongs in.
   1370    */
   1371   @Override
   1372   @MainThread
   1373   public void setDayGroup(long rowId, int dayGroup) {
   1374     dayGroups.put(rowId, dayGroup);
   1375   }
   1376 
   1377   /** Clears the day group associations on re-bind of the call log. */
   1378   @Override
   1379   @MainThread
   1380   public void clearDayGroups() {
   1381     dayGroups.clear();
   1382   }
   1383 
   1384   /**
   1385    * Retrieves the call Ids represented by the current call log row.
   1386    *
   1387    * @param cursor Call log cursor to retrieve call Ids from.
   1388    * @param groupSize Number of calls associated with the current call log row.
   1389    * @return Array of call Ids.
   1390    */
   1391   private long[] getCallIds(final Cursor cursor, final int groupSize) {
   1392     // We want to restore the position in the cursor at the end.
   1393     int startingPosition = cursor.getPosition();
   1394     long[] ids = new long[groupSize];
   1395     // Copy the ids of the rows in the group.
   1396     for (int index = 0; index < groupSize; ++index) {
   1397       ids[index] = cursor.getLong(CallLogQuery.ID);
   1398       cursor.moveToNext();
   1399     }
   1400     cursor.moveToPosition(startingPosition);
   1401     return ids;
   1402   }
   1403 
   1404   /**
   1405    * Determines the description for a day group.
   1406    *
   1407    * @param group The day group to retrieve the description for.
   1408    * @return The day group description.
   1409    */
   1410   private CharSequence getGroupDescription(int group) {
   1411     if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) {
   1412       return activity.getResources().getString(R.string.call_log_header_today);
   1413     } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) {
   1414       return activity.getResources().getString(R.string.call_log_header_yesterday);
   1415     } else {
   1416       return activity.getResources().getString(R.string.call_log_header_other);
   1417     }
   1418   }
   1419 
   1420   @NonNull
   1421   private EnrichedCallManager getEnrichedCallManager() {
   1422     return EnrichedCallComponent.get(activity).getEnrichedCallManager();
   1423   }
   1424 
   1425   @NonNull
   1426   private Duo getDuo() {
   1427     return DuoComponent.get(activity).getDuo();
   1428   }
   1429 
   1430   @Override
   1431   public void onDuoStateChanged() {
   1432     notifyDataSetChanged();
   1433   }
   1434 
   1435   public void onAllSelected() {
   1436     selectAllMode = true;
   1437     deselectAllMode = false;
   1438     selectedItems.clear();
   1439     for (int i = 0; i < getItemCount(); i++) {
   1440       Cursor c = (Cursor) getItem(i);
   1441       if (c != null) {
   1442         Assert.checkArgument(CallLogQuery.VOICEMAIL_URI == c.getColumnIndex("voicemail_uri"));
   1443         String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
   1444         selectedItems.put(getVoicemailId(voicemailUri), voicemailUri);
   1445       }
   1446     }
   1447     updateActionBar();
   1448     notifyDataSetChanged();
   1449   }
   1450 
   1451   public void onAllDeselected() {
   1452     selectAllMode = false;
   1453     deselectAllMode = true;
   1454     selectedItems.clear();
   1455     updateActionBar();
   1456     notifyDataSetChanged();
   1457   }
   1458 
   1459   /** Interface used to initiate a refresh of the content. */
   1460   public interface CallFetcher {
   1461 
   1462     void fetchCalls();
   1463   }
   1464 
   1465   /** Interface used to allow single tap multi select for contact photos. */
   1466   public interface OnActionModeStateChangedListener {
   1467 
   1468     void onActionModeStateChanged(boolean isEnabled);
   1469 
   1470     boolean isActionModeStateEnabled();
   1471   }
   1472 
   1473   /** Interface used to hide the fragments. */
   1474   public interface MultiSelectRemoveView {
   1475 
   1476     void showMultiSelectRemoveView(boolean show);
   1477 
   1478     void setSelectAllModeToFalse();
   1479 
   1480     void tapSelectAll();
   1481   }
   1482 }
   1483