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.calllog;
18 
19 import com.google.common.annotations.VisibleForTesting;
20 
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.SharedPreferences;
24 import android.content.res.Resources;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.os.Trace;
29 import android.preference.PreferenceManager;
30 import android.provider.CallLog;
31 import android.provider.ContactsContract.CommonDataKinds.Phone;
32 import android.support.v7.widget.RecyclerView;
33 import android.support.v7.widget.RecyclerView.ViewHolder;
34 import android.telecom.PhoneAccountHandle;
35 import android.telephony.PhoneNumberUtils;
36 import android.telephony.TelephonyManager;
37 import android.text.TextUtils;
38 import android.util.ArrayMap;
39 import android.view.LayoutInflater;
40 import android.view.View;
41 import android.view.View.AccessibilityDelegate;
42 import android.view.ViewGroup;
43 import android.view.accessibility.AccessibilityEvent;
44 
45 import com.android.contacts.common.ContactsUtils;
46 import com.android.contacts.common.compat.CompatUtils;
47 import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
48 import com.android.contacts.common.preference.ContactsPreferences;
49 import com.android.contacts.common.util.PermissionsUtil;
50 import com.android.dialer.DialtactsActivity;
51 import com.android.dialer.PhoneCallDetails;
52 import com.android.dialer.R;
53 import com.android.dialer.calllog.calllogcache.CallLogCache;
54 import com.android.dialer.contactinfo.ContactInfoCache;
55 import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
56 import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
57 import com.android.dialer.database.VoicemailArchiveContract;
58 import com.android.dialer.filterednumber.BlockNumberDialogFragment.Callback;
59 import com.android.dialer.logging.InteractionEvent;
60 import com.android.dialer.logging.Logger;
61 import com.android.dialer.service.ExtendedBlockingButtonRenderer;
62 import com.android.dialer.util.PhoneNumberUtil;
63 import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
64 
65 import java.util.HashMap;
66 import java.util.Map;
67 
68 /**
69  * Adapter class to fill in data for the Call Log.
70  */
71 public class CallLogAdapter extends GroupingListAdapter
72         implements CallLogGroupBuilder.GroupCreator,
73                 VoicemailPlaybackPresenter.OnVoicemailDeletedListener,
74                 ExtendedBlockingButtonRenderer.Listener {
75 
76     // Types of activities the call log adapter is used for
77     public static final int ACTIVITY_TYPE_CALL_LOG = 1;
78     public static final int ACTIVITY_TYPE_ARCHIVE = 2;
79     public static final int ACTIVITY_TYPE_DIALTACTS = 3;
80 
81     /** Interface used to initiate a refresh of the content. */
82     public interface CallFetcher {
fetchCalls()83         public void fetchCalls();
84     }
85 
86     private static final int NO_EXPANDED_LIST_ITEM = -1;
87     // ConcurrentHashMap doesn't store null values. Use this value for numbers which aren't blocked.
88     private static final int NOT_BLOCKED = -1;
89 
90     private static final int VOICEMAIL_PROMO_CARD_POSITION = 0;
91 
92     protected static final int VIEW_TYPE_NORMAL = 0;
93     private static final int VIEW_TYPE_VOICEMAIL_PROMO_CARD = 1;
94 
95     /**
96      * The key for the show voicemail promo card preference which will determine whether the promo
97      * card was permanently dismissed or not.
98      */
99     private static final String SHOW_VOICEMAIL_PROMO_CARD = "show_voicemail_promo_card";
100     private static final boolean SHOW_VOICEMAIL_PROMO_CARD_DEFAULT = true;
101 
102     protected final Context mContext;
103     private final ContactInfoHelper mContactInfoHelper;
104     protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
105     private final CallFetcher mCallFetcher;
106     private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
107     private final Map<String, Boolean> mBlockedNumberCache = new ArrayMap<>();
108 
109     protected ContactInfoCache mContactInfoCache;
110 
111     private final int mActivityType;
112 
113     private static final String KEY_EXPANDED_POSITION = "expanded_position";
114     private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id";
115 
116     // Tracks the position of the currently expanded list item.
117     private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
118     // Tracks the rowId of the currently expanded list item, so the position can be updated if there
119     // are any changes to the call log entries, such as additions or removals.
120     private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
121     private int mHiddenPosition = RecyclerView.NO_POSITION;
122     private Uri mHiddenItemUri = null;
123     private boolean mPendingHide = false;
124 
125     /**
126      *  Hashmap, keyed by call Id, used to track the day group for a call.  As call log entries are
127      *  put into the primary call groups in {@link com.android.dialer.calllog.CallLogGroupBuilder},
128      *  they are also assigned a secondary "day group".  This hashmap tracks the day group assigned
129      *  to all calls in the call log.  This information is used to trigger the display of a day
130      *  group header above the call log entry at the start of a day group.
131      *  Note: Multiple calls are grouped into a single primary "call group" in the call log, and
132      *  the cursor used to bind rows includes all of these calls.  When determining if a day group
133      *  change has occurred it is necessary to look at the last entry in the call log to determine
134      *  its day group.  This hashmap provides a means of determining the previous day group without
135      *  having to reverse the cursor to the start of the previous day call log entry.
136      */
137     private HashMap<Long, Integer> mDayGroups = new HashMap<>();
138 
139     private boolean mLoading = true;
140 
141     private SharedPreferences mPrefs;
142 
143     private ContactsPreferences mContactsPreferences;
144 
145     protected boolean mShowVoicemailPromoCard = false;
146 
147     /** Instance of helper class for managing views. */
148     private final CallLogListItemHelper mCallLogListItemHelper;
149 
150     /** Cache for repeated requests to Telecom/Telephony. */
151     protected final CallLogCache mCallLogCache;
152 
153     /** Helper to group call log entries. */
154     private final CallLogGroupBuilder mCallLogGroupBuilder;
155 
156     /**
157      * The OnClickListener used to expand or collapse the action buttons of a call log entry.
158      */
159     private final View.OnClickListener mExpandCollapseListener = new View.OnClickListener() {
160         @Override
161         public void onClick(View v) {
162             CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag();
163             if (viewHolder == null) {
164                 return;
165             }
166 
167             if (mVoicemailPlaybackPresenter != null) {
168                 // Always reset the voicemail playback state on expand or collapse.
169                 mVoicemailPlaybackPresenter.resetAll();
170             }
171 
172             if (viewHolder.getAdapterPosition() == mCurrentlyExpandedPosition) {
173                 // Hide actions, if the clicked item is the expanded item.
174                 viewHolder.showActions(false);
175 
176                 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
177                 mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
178             } else {
179                 if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) {
180                     CallLogAsyncTaskUtil.markCallAsRead(mContext, viewHolder.callIds);
181                     if (mActivityType == ACTIVITY_TYPE_DIALTACTS) {
182                         ((DialtactsActivity) v.getContext()).updateTabUnreadCounts();
183                     }
184                 }
185                 expandViewHolderActions(viewHolder);
186             }
187 
188         }
189     };
190 
191     /**
192      * Click handler used to dismiss the promo card when the user taps the "ok" button.
193      */
194     private final View.OnClickListener mOkActionListener = new View.OnClickListener() {
195         @Override
196         public void onClick(View view) {
197             dismissVoicemailPromoCard();
198         }
199     };
200 
201     /**
202      * Click handler used to send the user to the voicemail settings screen and then dismiss the
203      * promo card.
204      */
205     private final View.OnClickListener mVoicemailSettingsActionListener =
206             new View.OnClickListener() {
207         @Override
208         public void onClick(View view) {
209             Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
210             mContext.startActivity(intent);
211             dismissVoicemailPromoCard();
212         }
213     };
214 
expandViewHolderActions(CallLogListItemViewHolder viewHolder)215     private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
216         // If another item is expanded, notify it that it has changed. Its actions will be
217         // hidden when it is re-binded because we change mCurrentlyExpandedPosition below.
218         if (mCurrentlyExpandedPosition != RecyclerView.NO_POSITION) {
219             notifyItemChanged(mCurrentlyExpandedPosition);
220         }
221         // Show the actions for the clicked list item.
222         viewHolder.showActions(true);
223         mCurrentlyExpandedPosition = viewHolder.getAdapterPosition();
224         mCurrentlyExpandedRowId = viewHolder.rowId;
225     }
226 
227     /**
228      * Expand the actions on a list item when focused in Talkback mode, to aid discoverability.
229      */
230     private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() {
231         @Override
232         public boolean onRequestSendAccessibilityEvent(
233                 ViewGroup host, View child, AccessibilityEvent event) {
234             if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
235                 // Only expand if actions are not already expanded, because triggering the expand
236                 // function on clicks causes the action views to lose the focus indicator.
237                 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) host.getTag();
238                 if (mCurrentlyExpandedPosition != viewHolder.getAdapterPosition()) {
239                     if (mVoicemailPlaybackPresenter != null) {
240                         // Always reset the voicemail playback state on expand.
241                         mVoicemailPlaybackPresenter.resetAll();
242                     }
243 
244                     expandViewHolderActions((CallLogListItemViewHolder) host.getTag());
245                 }
246             }
247             return super.onRequestSendAccessibilityEvent(host, child, event);
248         }
249     };
250 
251     protected final OnContactInfoChangedListener mOnContactInfoChangedListener =
252             new OnContactInfoChangedListener() {
253                 @Override
254                 public void onContactInfoChanged() {
255                     notifyDataSetChanged();
256                 }
257             };
258 
CallLogAdapter( Context context, CallFetcher callFetcher, ContactInfoHelper contactInfoHelper, VoicemailPlaybackPresenter voicemailPlaybackPresenter, int activityType)259     public CallLogAdapter(
260             Context context,
261             CallFetcher callFetcher,
262             ContactInfoHelper contactInfoHelper,
263             VoicemailPlaybackPresenter voicemailPlaybackPresenter,
264             int activityType) {
265         super(context);
266 
267         mContext = context;
268         mCallFetcher = callFetcher;
269         mContactInfoHelper = contactInfoHelper;
270         mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
271         if (mVoicemailPlaybackPresenter != null) {
272             mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this);
273         }
274 
275         mActivityType = activityType;
276 
277         mContactInfoCache = new ContactInfoCache(
278                 mContactInfoHelper, mOnContactInfoChangedListener);
279         if (!PermissionsUtil.hasContactsPermissions(context)) {
280             mContactInfoCache.disableRequestProcessing();
281         }
282 
283         Resources resources = mContext.getResources();
284         CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
285 
286         mCallLogCache = CallLogCache.getCallLogCache(mContext);
287 
288         PhoneCallDetailsHelper phoneCallDetailsHelper =
289                 new PhoneCallDetailsHelper(mContext, resources, mCallLogCache);
290         mCallLogListItemHelper =
291                 new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache);
292         mCallLogGroupBuilder = new CallLogGroupBuilder(this);
293         mFilteredNumberAsyncQueryHandler =
294                 new FilteredNumberAsyncQueryHandler(mContext.getContentResolver());
295 
296         mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
297         mContactsPreferences = new ContactsPreferences(mContext);
298         maybeShowVoicemailPromoCard();
299     }
300 
onSaveInstanceState(Bundle outState)301     public void onSaveInstanceState(Bundle outState) {
302         outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition);
303         outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId);
304     }
305 
onRestoreInstanceState(Bundle savedInstanceState)306     public void onRestoreInstanceState(Bundle savedInstanceState) {
307         if (savedInstanceState != null) {
308             mCurrentlyExpandedPosition =
309                     savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION);
310             mCurrentlyExpandedRowId =
311                     savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM);
312         }
313     }
314 
315     @Override
onBlockedNumber(String number,String countryIso)316     public void onBlockedNumber(String number,String countryIso) {
317         String cacheKey = PhoneNumberUtils.formatNumberToE164(number, countryIso);
318         if (!TextUtils.isEmpty(cacheKey)) {
319             mBlockedNumberCache.put(cacheKey, true);
320             notifyDataSetChanged();
321         }
322     }
323 
324     @Override
onUnblockedNumber( String number, String countryIso)325     public void onUnblockedNumber( String number, String countryIso) {
326         String cacheKey = PhoneNumberUtils.formatNumberToE164(number, countryIso);
327         if (!TextUtils.isEmpty(cacheKey)) {
328             mBlockedNumberCache.put(cacheKey, false);
329             notifyDataSetChanged();
330         }
331     }
332 
333     /**
334      * Requery on background thread when {@link Cursor} changes.
335      */
336     @Override
onContentChanged()337     protected void onContentChanged() {
338         mCallFetcher.fetchCalls();
339     }
340 
setLoading(boolean loading)341     public void setLoading(boolean loading) {
342         mLoading = loading;
343     }
344 
isEmpty()345     public boolean isEmpty() {
346         if (mLoading) {
347             // We don't want the empty state to show when loading.
348             return false;
349         } else {
350             return getItemCount() == 0;
351         }
352     }
353 
invalidateCache()354     public void invalidateCache() {
355         mContactInfoCache.invalidate();
356     }
357 
onResume()358     public void onResume() {
359         if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
360             mContactInfoCache.start();
361         }
362         mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
363     }
364 
onPause()365     public void onPause() {
366         pauseCache();
367 
368         if (mHiddenItemUri != null) {
369             CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null);
370         }
371     }
372 
373     @VisibleForTesting
pauseCache()374     /* package */ void pauseCache() {
375         mContactInfoCache.stop();
376         mCallLogCache.reset();
377     }
378 
379     @Override
addGroups(Cursor cursor)380     protected void addGroups(Cursor cursor) {
381         mCallLogGroupBuilder.addGroups(cursor);
382     }
383 
384     @Override
addVoicemailGroups(Cursor cursor)385     public void addVoicemailGroups(Cursor cursor) {
386         mCallLogGroupBuilder.addVoicemailGroups(cursor);
387     }
388 
389     @Override
onCreateViewHolder(ViewGroup parent, int viewType)390     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
391         if (viewType == VIEW_TYPE_VOICEMAIL_PROMO_CARD) {
392             return createVoicemailPromoCardViewHolder(parent);
393         }
394         return createCallLogEntryViewHolder(parent);
395     }
396 
397     /**
398      * Creates a new call log entry {@link ViewHolder}.
399      *
400      * @param parent the parent view.
401      * @return The {@link ViewHolder}.
402      */
createCallLogEntryViewHolder(ViewGroup parent)403     private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) {
404         LayoutInflater inflater = LayoutInflater.from(mContext);
405         View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
406         CallLogListItemViewHolder viewHolder = CallLogListItemViewHolder.create(
407                 view,
408                 mContext,
409                 this,
410                 mExpandCollapseListener,
411                 mCallLogCache,
412                 mCallLogListItemHelper,
413                 mVoicemailPlaybackPresenter,
414                 mFilteredNumberAsyncQueryHandler,
415                 new Callback() {
416                     @Override
417                     public void onFilterNumberSuccess() {
418                         Logger.logInteraction(
419                                 InteractionEvent.BLOCK_NUMBER_CALL_LOG);
420                     }
421 
422                     @Override
423                     public void onUnfilterNumberSuccess() {
424                         Logger.logInteraction(
425                                 InteractionEvent.UNBLOCK_NUMBER_CALL_LOG);
426                     }
427 
428                     @Override
429                     public void onChangeFilteredNumberUndo() {}
430                 }, mActivityType == ACTIVITY_TYPE_ARCHIVE);
431 
432         viewHolder.callLogEntryView.setTag(viewHolder);
433         viewHolder.callLogEntryView.setAccessibilityDelegate(mAccessibilityDelegate);
434 
435         viewHolder.primaryActionView.setTag(viewHolder);
436 
437         return viewHolder;
438     }
439 
440     /**
441      * Binds the views in the entry to the data in the call log.
442      * TODO: This gets called 20-30 times when Dialer starts up for a single call log entry and
443      * should not. It invokes cross-process methods and the repeat execution can get costly.
444      *
445      * @param viewHolder The view corresponding to this entry.
446      * @param position The position of the entry.
447      */
448     @Override
onBindViewHolder(ViewHolder viewHolder, int position)449     public void onBindViewHolder(ViewHolder viewHolder, int position) {
450         Trace.beginSection("onBindViewHolder: " + position);
451 
452         switch (getItemViewType(position)) {
453             case VIEW_TYPE_VOICEMAIL_PROMO_CARD:
454                 bindVoicemailPromoCardViewHolder(viewHolder);
455                 break;
456             default:
457                 bindCallLogListViewHolder(viewHolder, position);
458                 break;
459         }
460 
461         Trace.endSection();
462     }
463 
464     /**
465      * Binds the promo card view holder.
466      *
467      * @param viewHolder The promo card view holder.
468      */
bindVoicemailPromoCardViewHolder(ViewHolder viewHolder)469     protected void bindVoicemailPromoCardViewHolder(ViewHolder viewHolder) {
470         PromoCardViewHolder promoCardViewHolder = (PromoCardViewHolder) viewHolder;
471 
472         promoCardViewHolder.getSecondaryActionView()
473                 .setOnClickListener(mVoicemailSettingsActionListener);
474         promoCardViewHolder.getPrimaryActionView().setOnClickListener(mOkActionListener);
475     }
476 
477     /**
478      * Binds the view holder for the call log list item view.
479      *
480      * @param viewHolder The call log list item view holder.
481      * @param position The position of the list item.
482      */
483 
bindCallLogListViewHolder(ViewHolder viewHolder, int position)484     private void bindCallLogListViewHolder(ViewHolder viewHolder, int position) {
485         Cursor c = (Cursor) getItem(position);
486         if (c == null) {
487             return;
488         }
489 
490         int count = getGroupSize(position);
491 
492         final String number = c.getString(CallLogQuery.NUMBER);
493         final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
494         final String postDialDigits = CompatUtils.isNCompatible()
495                 && mActivityType != ACTIVITY_TYPE_ARCHIVE ?
496                 c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
497         final String viaNumber = CompatUtils.isNCompatible()
498                 && mActivityType != ACTIVITY_TYPE_ARCHIVE ?
499                 c.getString(CallLogQuery.VIA_NUMBER) : "";
500         final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION);
501         final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount(
502                 c.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME),
503                 c.getString(CallLogQuery.ACCOUNT_ID));
504         final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(c);
505         final boolean isVoicemailNumber =
506                 mCallLogCache.isVoicemailNumber(accountHandle, number);
507 
508         // Note: Binding of the action buttons is done as required in configureActionViews when the
509         // user expands the actions ViewStub.
510 
511         ContactInfo info = ContactInfo.EMPTY;
512         if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !isVoicemailNumber) {
513             // Lookup contacts with this number
514             info = mContactInfoCache.getValue(number + postDialDigits,
515                     countryIso, cachedContactInfo);
516         }
517         CharSequence formattedNumber = info.formattedNumber == null
518                 ? null : PhoneNumberUtilsCompat.createTtsSpannable(info.formattedNumber);
519 
520         final PhoneCallDetails details = new PhoneCallDetails(
521                 mContext, number, numberPresentation, formattedNumber,
522                 postDialDigits, isVoicemailNumber);
523         details.viaNumber = viaNumber;
524         details.accountHandle = accountHandle;
525         details.countryIso = countryIso;
526         details.date = c.getLong(CallLogQuery.DATE);
527         details.duration = c.getLong(CallLogQuery.DURATION);
528         details.features = getCallFeatures(c, count);
529         details.geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
530         details.transcription = c.getString(CallLogQuery.TRANSCRIPTION);
531         details.callTypes = getCallTypes(c, count);
532 
533         if (!c.isNull(CallLogQuery.DATA_USAGE)) {
534             details.dataUsage = c.getLong(CallLogQuery.DATA_USAGE);
535         }
536 
537         if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) {
538             details.contactUri = info.lookupUri;
539             details.namePrimary = info.name;
540             details.nameAlternative = info.nameAlternative;
541             details.nameDisplayOrder = mContactsPreferences.getDisplayOrder();
542             details.numberType = info.type;
543             details.numberLabel = info.label;
544             details.photoUri = info.photoUri;
545             details.sourceType = info.sourceType;
546             details.objectId = info.objectId;
547             details.contactUserType = info.userType;
548         }
549 
550         final CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
551         views.info = info;
552         views.rowId = c.getLong(CallLogQuery.ID);
553         // Store values used when the actions ViewStub is inflated on expansion.
554         views.number = number;
555         views.postDialDigits = details.postDialDigits;
556         views.displayNumber = details.displayNumber;
557         views.numberPresentation = numberPresentation;
558 
559         views.accountHandle = accountHandle;
560         // Stash away the Ids of the calls so that we can support deleting a row in the call log.
561         views.callIds = getCallIds(c, count);
562         views.isBusiness = mContactInfoHelper.isBusiness(info.sourceType);
563         views.numberType = (String) Phone.getTypeLabel(mContext.getResources(), details.numberType,
564                 details.numberLabel);
565         // Default case: an item in the call log.
566         views.primaryActionView.setVisibility(View.VISIBLE);
567         views.workIconView.setVisibility(
568                 details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE);
569 
570         // Check if the day group has changed and display a header if necessary.
571         int currentGroup = getDayGroupForCall(views.rowId);
572         int previousGroup = getPreviousDayGroup(c);
573         if (currentGroup != previousGroup) {
574             views.dayGroupHeader.setVisibility(View.VISIBLE);
575             views.dayGroupHeader.setText(getGroupDescription(currentGroup));
576         } else {
577             views.dayGroupHeader.setVisibility(View.GONE);
578         }
579 
580         if (mActivityType == ACTIVITY_TYPE_ARCHIVE) {
581             views.callType = CallLog.Calls.VOICEMAIL_TYPE;
582             views.voicemailUri = VoicemailArchiveContract.VoicemailArchive.buildWithId(c.getInt(
583                     c.getColumnIndex(VoicemailArchiveContract.VoicemailArchive._ID)))
584                     .toString();
585 
586         } else {
587             if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE ||
588                     details.callTypes[0] == CallLog.Calls.MISSED_TYPE) {
589                 details.isRead = c.getInt(CallLogQuery.IS_READ) == 1;
590             }
591             views.callType = c.getInt(CallLogQuery.CALL_TYPE);
592             views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
593         }
594 
595         mCallLogListItemHelper.setPhoneCallDetails(views, details);
596 
597         if (mCurrentlyExpandedRowId == views.rowId) {
598             // In case ViewHolders were added/removed, update the expanded position if the rowIds
599             // match so that we can restore the correct expanded state on rebind.
600             mCurrentlyExpandedPosition = position;
601             views.showActions(true);
602         } else {
603             views.showActions(false);
604         }
605         views.updatePhoto();
606 
607         mCallLogListItemHelper.setPhoneCallDetails(views, details);
608     }
609 
getPreferredDisplayName(ContactInfo contactInfo)610     private String getPreferredDisplayName(ContactInfo contactInfo) {
611         if (mContactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY ||
612                 TextUtils.isEmpty(contactInfo.nameAlternative)) {
613             return contactInfo.name;
614         }
615         return contactInfo.nameAlternative;
616     }
617 
618     @Override
getItemCount()619     public int getItemCount() {
620         return super.getItemCount() + (mShowVoicemailPromoCard ? 1 : 0)
621                 - (mHiddenPosition != RecyclerView.NO_POSITION ? 1 : 0);
622     }
623 
624     @Override
getItemViewType(int position)625     public int getItemViewType(int position) {
626         if (position == VOICEMAIL_PROMO_CARD_POSITION && mShowVoicemailPromoCard) {
627             return VIEW_TYPE_VOICEMAIL_PROMO_CARD;
628         }
629         return super.getItemViewType(position);
630     }
631 
632     /**
633      * Retrieves an item at the specified position, taking into account the presence of a promo
634      * card.
635      *
636      * @param position The position to retrieve.
637      * @return The item at that position.
638      */
639     @Override
getItem(int position)640     public Object getItem(int position) {
641         return super.getItem(position - (mShowVoicemailPromoCard ? 1 : 0)
642                 + ((mHiddenPosition != RecyclerView.NO_POSITION && position >= mHiddenPosition)
643                 ? 1 : 0));
644     }
645 
646     @Override
getGroupSize(int position)647     public int getGroupSize(int position) {
648         return super.getGroupSize(position - (mShowVoicemailPromoCard ? 1 : 0));
649     }
650 
isCallLogActivity()651     protected boolean isCallLogActivity() {
652         return mActivityType == ACTIVITY_TYPE_CALL_LOG;
653     }
654 
655     /**
656      * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user
657      * clicks the delete button, the deleted item is temporarily hidden from the list. If a user
658      * clicks delete on a second item before the first item's undo option has expired, the first
659      * item is immediately deleted so that only one item can be "undoed" at a time.
660      */
661     @Override
onVoicemailDeleted(Uri uri)662     public void onVoicemailDeleted(Uri uri) {
663         if (mHiddenItemUri == null) {
664             // Immediately hide the currently expanded card.
665             mHiddenPosition = mCurrentlyExpandedPosition;
666             notifyDataSetChanged();
667         } else {
668             // This means that there was a previous item that was hidden in the UI but not
669             // yet deleted from the database (call it a "pending delete"). Delete this previous item
670             // now since it is only possible to do one "undo" at a time.
671             CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null);
672 
673             // Set pending hide action so that the current item is hidden only after the previous
674             // item is permanently deleted.
675             mPendingHide = true;
676         }
677 
678         collapseExpandedCard();
679 
680         // Save the new hidden item uri in case it needs to be deleted from the database when
681         // a user attempts to delete another item.
682         mHiddenItemUri = uri;
683     }
684 
collapseExpandedCard()685     private void collapseExpandedCard() {
686         mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
687         mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
688     }
689 
690     /**
691      * When the list is changing all stored position is no longer valid.
692      */
invalidatePositions()693     public void invalidatePositions() {
694         mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
695         mHiddenPosition = RecyclerView.NO_POSITION;
696     }
697 
698     /**
699      * When the user clicks "undo", the hidden item is unhidden.
700      */
701     @Override
onVoicemailDeleteUndo()702     public void onVoicemailDeleteUndo() {
703         mHiddenPosition = RecyclerView.NO_POSITION;
704         mHiddenItemUri = null;
705 
706         mPendingHide = false;
707         notifyDataSetChanged();
708     }
709 
710     /**
711      * This callback signifies that a database deletion has completed. This means that if there is
712      * an item pending deletion, it will be hidden because the previous item that was in "undo" mode
713      * has been removed from the database. Otherwise it simply resets the hidden state because there
714      * are no pending deletes and thus no hidden items.
715      */
716     @Override
onVoicemailDeletedInDatabase()717     public void onVoicemailDeletedInDatabase() {
718         if (mPendingHide) {
719             mHiddenPosition = mCurrentlyExpandedPosition;
720             mPendingHide = false;
721         } else {
722             // There should no longer be any hidden item because it has been deleted from the
723             // database.
724             mHiddenPosition = RecyclerView.NO_POSITION;
725             mHiddenItemUri = null;
726         }
727     }
728 
729     /**
730      * Retrieves the day group of the previous call in the call log.  Used to determine if the day
731      * group has changed and to trigger display of the day group text.
732      *
733      * @param cursor The call log cursor.
734      * @return The previous day group, or DAY_GROUP_NONE if this is the first call.
735      */
getPreviousDayGroup(Cursor cursor)736     private int getPreviousDayGroup(Cursor cursor) {
737         // We want to restore the position in the cursor at the end.
738         int startingPosition = cursor.getPosition();
739         int dayGroup = CallLogGroupBuilder.DAY_GROUP_NONE;
740         if (cursor.moveToPrevious()) {
741             // If the previous entry is hidden (deleted in the UI but not in the database), skip it
742             // and check the card above it. A list with the voicemail promo card at the top will be
743             // 1-indexed because the 0th index is the promo card iteself.
744             int previousViewPosition = mShowVoicemailPromoCard ? startingPosition :
745                 startingPosition - 1;
746             if (previousViewPosition != mHiddenPosition ||
747                     (previousViewPosition == mHiddenPosition && cursor.moveToPrevious())) {
748                 long previousRowId = cursor.getLong(CallLogQuery.ID);
749                 dayGroup = getDayGroupForCall(previousRowId);
750             }
751         }
752         cursor.moveToPosition(startingPosition);
753         return dayGroup;
754     }
755 
756     /**
757      * Given a call Id, look up the day group that the call belongs to.  The day group data is
758      * populated in {@link com.android.dialer.calllog.CallLogGroupBuilder}.
759      *
760      * @param callId The call to retrieve the day group for.
761      * @return The day group for the call.
762      */
getDayGroupForCall(long callId)763     private int getDayGroupForCall(long callId) {
764         if (mDayGroups.containsKey(callId)) {
765             return mDayGroups.get(callId);
766         }
767         return CallLogGroupBuilder.DAY_GROUP_NONE;
768     }
769 
770     /**
771      * Returns the call types for the given number of items in the cursor.
772      * <p>
773      * It uses the next {@code count} rows in the cursor to extract the types.
774      * <p>
775      * It position in the cursor is unchanged by this function.
776      */
getCallTypes(Cursor cursor, int count)777     private int[] getCallTypes(Cursor cursor, int count) {
778         if (mActivityType == ACTIVITY_TYPE_ARCHIVE) {
779             return new int[] {CallLog.Calls.VOICEMAIL_TYPE};
780         }
781         int position = cursor.getPosition();
782         int[] callTypes = new int[count];
783         for (int index = 0; index < count; ++index) {
784             callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
785             cursor.moveToNext();
786         }
787         cursor.moveToPosition(position);
788         return callTypes;
789     }
790 
791     /**
792      * Determine the features which were enabled for any of the calls that make up a call log
793      * entry.
794      *
795      * @param cursor The cursor.
796      * @param count The number of calls for the current call log entry.
797      * @return The features.
798      */
getCallFeatures(Cursor cursor, int count)799     private int getCallFeatures(Cursor cursor, int count) {
800         int features = 0;
801         int position = cursor.getPosition();
802         for (int index = 0; index < count; ++index) {
803             features |= cursor.getInt(CallLogQuery.FEATURES);
804             cursor.moveToNext();
805         }
806         cursor.moveToPosition(position);
807         return features;
808     }
809 
810     /**
811      * Sets whether processing of requests for contact details should be enabled.
812      *
813      * This method should be called in tests to disable such processing of requests when not
814      * needed.
815      */
816     @VisibleForTesting
disableRequestProcessingForTest()817     void disableRequestProcessingForTest() {
818         // TODO: Remove this and test the cache directly.
819         mContactInfoCache.disableRequestProcessing();
820     }
821 
822     @VisibleForTesting
injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo)823     void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
824         // TODO: Remove this and test the cache directly.
825         mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo);
826     }
827 
828     /**
829      * Stores the day group associated with a call in the call log.
830      *
831      * @param rowId The row Id of the current call.
832      * @param dayGroup The day group the call belongs in.
833      */
834     @Override
setDayGroup(long rowId, int dayGroup)835     public void setDayGroup(long rowId, int dayGroup) {
836         if (!mDayGroups.containsKey(rowId)) {
837             mDayGroups.put(rowId, dayGroup);
838         }
839     }
840 
841     /**
842      * Clears the day group associations on re-bind of the call log.
843      */
844     @Override
clearDayGroups()845     public void clearDayGroups() {
846         mDayGroups.clear();
847     }
848 
849     /**
850      * Retrieves the call Ids represented by the current call log row.
851      *
852      * @param cursor Call log cursor to retrieve call Ids from.
853      * @param groupSize Number of calls associated with the current call log row.
854      * @return Array of call Ids.
855      */
getCallIds(final Cursor cursor, final int groupSize)856     private long[] getCallIds(final Cursor cursor, final int groupSize) {
857         // We want to restore the position in the cursor at the end.
858         int startingPosition = cursor.getPosition();
859         long[] ids = new long[groupSize];
860         // Copy the ids of the rows in the group.
861         for (int index = 0; index < groupSize; ++index) {
862             ids[index] = cursor.getLong(CallLogQuery.ID);
863             cursor.moveToNext();
864         }
865         cursor.moveToPosition(startingPosition);
866         return ids;
867     }
868 
869     /**
870      * Determines the description for a day group.
871      *
872      * @param group The day group to retrieve the description for.
873      * @return The day group description.
874      */
getGroupDescription(int group)875     private CharSequence getGroupDescription(int group) {
876        if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) {
877            return mContext.getResources().getString(R.string.call_log_header_today);
878        } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) {
879            return mContext.getResources().getString(R.string.call_log_header_yesterday);
880        } else {
881            return mContext.getResources().getString(R.string.call_log_header_other);
882        }
883     }
884 
885     /**
886      * Determines if the voicemail promo card should be shown or not.  The voicemail promo card will
887      * be shown as the first item in the voicemail tab.
888      */
maybeShowVoicemailPromoCard()889     private void maybeShowVoicemailPromoCard() {
890         boolean showPromoCard = mPrefs.getBoolean(SHOW_VOICEMAIL_PROMO_CARD,
891                 SHOW_VOICEMAIL_PROMO_CARD_DEFAULT);
892         mShowVoicemailPromoCard = mActivityType != ACTIVITY_TYPE_ARCHIVE &&
893                 (mVoicemailPlaybackPresenter != null) && showPromoCard;
894     }
895 
896     /**
897      * Dismisses the voicemail promo card and refreshes the call log.
898      */
dismissVoicemailPromoCard()899     private void dismissVoicemailPromoCard() {
900         mPrefs.edit().putBoolean(SHOW_VOICEMAIL_PROMO_CARD, false).apply();
901         mShowVoicemailPromoCard = false;
902         notifyItemRemoved(VOICEMAIL_PROMO_CARD_POSITION);
903     }
904 
905     /**
906      * Creates the view holder for the voicemail promo card.
907      *
908      * @param parent The parent view.
909      * @return The {@link ViewHolder}.
910      */
createVoicemailPromoCardViewHolder(ViewGroup parent)911     protected ViewHolder createVoicemailPromoCardViewHolder(ViewGroup parent) {
912         LayoutInflater inflater = LayoutInflater.from(mContext);
913         View view = inflater.inflate(R.layout.voicemail_promo_card, parent, false);
914 
915         PromoCardViewHolder viewHolder = PromoCardViewHolder.create(view);
916         return viewHolder;
917     }
918 }
919