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