• 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.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 
showDeleteSelectedItemsDialog()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 
deleteSelectedItems(SparseArray<String> voicemailsToDelete)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
getExpandCollapseListener()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
getOnScrollListener()420   public RecyclerView.OnScrollListener getOnScrollListener() {
421     return null;
422   }
423 
checkMarkCallLogEntry(CallLogListItemViewHolder viewHolder)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 
announceforAccessibility(View view, String announcement)435   private void announceforAccessibility(View view, String announcement) {
436     if (view != null) {
437       view.announceForAccessibility(announcement);
438     }
439   }
440 
updateActionBar()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 
uncheckMarkCallLogEntry(CallLogListItemViewHolder viewHolder, int id)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 
getVoicemailId(String voicemailUri)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 
CallLogAdapter( Activity activity, ViewGroup alertContainer, CallFetcher callFetcher, MultiSelectRemoveView multiSelectRemoveView, OnActionModeStateChangedListener actionModeStateChangedListener, CallLogCache callLogCache, ContactInfoCache contactInfoCache, VoicemailPlaybackPresenter voicemailPlaybackPresenter, @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler, int activityType)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 
expandViewHolderActions(CallLogListItemViewHolder viewHolder)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 
onSaveInstanceState(Bundle outState)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 
onRestoreInstanceState(Bundle savedInstanceState)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
onContentChanged()656   protected void onContentChanged() {
657     callFetcher.fetchCalls();
658   }
659 
setLoading(boolean loading)660   public void setLoading(boolean loading) {
661     this.loading = loading;
662   }
663 
isEmpty()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 
clearFilteredNumbersCache()673   public void clearFilteredNumbersCache() {
674     filteredNumberAsyncQueryHandler.clearCache();
675   }
676 
onResume()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 
onPause()687   public void onPause() {
688     getDuo().unregisterListener(this);
689     pauseCache();
690     for (Uri uri : hiddenItemUris) {
691       CallLogAsyncTaskUtil.deleteVoicemail(activity, uri, null);
692     }
693   }
694 
onStop()695   public void onStop() {
696     getEnrichedCallManager().clearCachedData();
697   }
698 
getAlertManager()699   public CallLogAlertManager getAlertManager() {
700     return callLogAlertManager;
701   }
702 
703   @VisibleForTesting
pauseCache()704   /* package */ void pauseCache() {
705     contactInfoCache.stop();
706     callLogCache.reset();
707   }
708 
709   @Override
addGroups(Cursor cursor)710   protected void addGroups(Cursor cursor) {
711     callLogGroupBuilder.addGroups(cursor);
712   }
713 
714   @Override
onCreateViewHolder(ViewGroup parent, int viewType)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    */
createCallLogEntryViewHolder(ViewGroup parent)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
onBindViewHolder(ViewHolder viewHolder, int position)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
onViewRecycled(ViewHolder viewHolder)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
onViewAttachedToWindow(ViewHolder viewHolder)786   public void onViewAttachedToWindow(ViewHolder viewHolder) {
787     if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
788       ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = true;
789     }
790   }
791 
792   @Override
onViewDetachedFromWindow(ViewHolder viewHolder)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    */
bindCallLogListViewHolder(final ViewHolder viewHolder, final int position)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 
updateCheckMarkedStatusOfEntry(CallLogListItemViewHolder views)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 
isHiddenRow(@ullable String number, long rowId)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 
loadAndRender( final CallLogListItemViewHolder viewHolder, final long rowId, final PhoneCallDetails details, final CallDetailsEntries callDetailsEntries)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
isCallComposerCapable(@ullable String number)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
createPhoneCallDetails( Cursor cursor, int count, final CallLogListItemViewHolder views)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
createCallDetailsEntries(Cursor cursor, int count)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
loadData(CallLogListItemViewHolder views, long rowId, PhoneCallDetails details)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 
getNumberType(Resources res, PhoneCallDetails details)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
render(CallLogListItemViewHolder views, PhoneCallDetails details, long rowId)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
getItemCount()1157   public int getItemCount() {
1158     return super.getItemCount() + (callLogAlertManager.isEmpty() ? 0 : 1);
1159   }
1160 
1161   @Override
getItemViewType(int position)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
getItem(int position)1176   public Object getItem(int position) {
1177     return super.getItem(position - (callLogAlertManager.isEmpty() ? 0 : 1));
1178   }
1179 
1180   @Override
getItemId(int position)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
getGroupSize(int position)1191   public int getGroupSize(int position) {
1192     return super.getGroupSize(position - (callLogAlertManager.isEmpty() ? 0 : 1));
1193   }
1194 
isCallLogActivity()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
onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri)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 
collapseExpandedCard()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. */
invalidatePositions()1223   public void invalidatePositions() {
1224     currentlyExpandedPosition = RecyclerView.NO_POSITION;
1225   }
1226 
1227   /** When the user clicks "undo", the hidden item is unhidden. */
1228   @Override
onVoicemailDeleteUndo(long rowId, int adapterPosition, Uri uri)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
onVoicemailDeletedInDatabase(long rowId, Uri uri)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    */
getPreviousDayGroup(Cursor cursor)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 
moveToPreviousNonHiddenRow(Cursor cursor)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
getCallbackAction(long callId)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
getDayGroup(long callId)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    */
getCallTypes(Cursor cursor, int count)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    */
getCallFeatures(Cursor cursor, int count)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
disableRequestProcessingForTest()1342   void disableRequestProcessingForTest() {
1343     // TODO: Remove this and test the cache directly.
1344     contactInfoCache.disableRequestProcessing();
1345   }
1346 
1347   @VisibleForTesting
injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo)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
setCallbackAction(long rowId, @CallbackAction int callbackAction)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
setDayGroup(long rowId, int dayGroup)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
clearDayGroups()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    */
getCallIds(final Cursor cursor, final int groupSize)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    */
getGroupDescription(int group)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
getEnrichedCallManager()1421   private EnrichedCallManager getEnrichedCallManager() {
1422     return EnrichedCallComponent.get(activity).getEnrichedCallManager();
1423   }
1424 
1425   @NonNull
getDuo()1426   private Duo getDuo() {
1427     return DuoComponent.get(activity).getDuo();
1428   }
1429 
1430   @Override
onDuoStateChanged()1431   public void onDuoStateChanged() {
1432     notifyDataSetChanged();
1433   }
1434 
onAllSelected()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 
onAllDeselected()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 
fetchCalls()1462     void fetchCalls();
1463   }
1464 
1465   /** Interface used to allow single tap multi select for contact photos. */
1466   public interface OnActionModeStateChangedListener {
1467 
onActionModeStateChanged(boolean isEnabled)1468     void onActionModeStateChanged(boolean isEnabled);
1469 
isActionModeStateEnabled()1470     boolean isActionModeStateEnabled();
1471   }
1472 
1473   /** Interface used to hide the fragments. */
1474   public interface MultiSelectRemoveView {
1475 
showMultiSelectRemoveView(boolean show)1476     void showMultiSelectRemoveView(boolean show);
1477 
setSelectAllModeToFalse()1478     void setSelectAllModeToFalse();
1479 
tapSelectAll()1480     void tapSelectAll();
1481   }
1482 }
1483