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