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