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
17package com.android.dialer.calllog;
18
19import com.google.common.annotations.VisibleForTesting;
20
21import android.content.Context;
22import android.content.Intent;
23import android.content.SharedPreferences;
24import android.content.res.Resources;
25import android.database.Cursor;
26import android.net.Uri;
27import android.os.Bundle;
28import android.os.Trace;
29import android.preference.PreferenceManager;
30import android.provider.CallLog;
31import android.provider.ContactsContract.CommonDataKinds.Phone;
32import android.support.v7.widget.RecyclerView;
33import android.support.v7.widget.RecyclerView.ViewHolder;
34import android.telecom.PhoneAccountHandle;
35import android.telephony.PhoneNumberUtils;
36import android.telephony.TelephonyManager;
37import android.text.TextUtils;
38import android.util.ArrayMap;
39import android.view.LayoutInflater;
40import android.view.View;
41import android.view.View.AccessibilityDelegate;
42import android.view.ViewGroup;
43import android.view.accessibility.AccessibilityEvent;
44
45import com.android.contacts.common.ContactsUtils;
46import com.android.contacts.common.compat.CompatUtils;
47import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
48import com.android.contacts.common.preference.ContactsPreferences;
49import com.android.contacts.common.util.PermissionsUtil;
50import com.android.dialer.DialtactsActivity;
51import com.android.dialer.PhoneCallDetails;
52import com.android.dialer.R;
53import com.android.dialer.calllog.calllogcache.CallLogCache;
54import com.android.dialer.contactinfo.ContactInfoCache;
55import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
56import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
57import com.android.dialer.database.VoicemailArchiveContract;
58import com.android.dialer.filterednumber.BlockNumberDialogFragment.Callback;
59import com.android.dialer.logging.InteractionEvent;
60import com.android.dialer.logging.Logger;
61import com.android.dialer.service.ExtendedBlockingButtonRenderer;
62import com.android.dialer.util.PhoneNumberUtil;
63import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
64
65import java.util.HashMap;
66import java.util.Map;
67
68/**
69 * Adapter class to fill in data for the Call Log.
70 */
71public 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 {
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
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
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
301    public void onSaveInstanceState(Bundle outState) {
302        outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition);
303        outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId);
304    }
305
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
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
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
337    protected void onContentChanged() {
338        mCallFetcher.fetchCalls();
339    }
340
341    public void setLoading(boolean loading) {
342        mLoading = loading;
343    }
344
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
354    public void invalidateCache() {
355        mContactInfoCache.invalidate();
356    }
357
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
365    public void onPause() {
366        pauseCache();
367
368        if (mHiddenItemUri != null) {
369            CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null);
370        }
371    }
372
373    @VisibleForTesting
374    /* package */ void pauseCache() {
375        mContactInfoCache.stop();
376        mCallLogCache.reset();
377    }
378
379    @Override
380    protected void addGroups(Cursor cursor) {
381        mCallLogGroupBuilder.addGroups(cursor);
382    }
383
384    @Override
385    public void addVoicemailGroups(Cursor cursor) {
386        mCallLogGroupBuilder.addVoicemailGroups(cursor);
387    }
388
389    @Override
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     */
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
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     */
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
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
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
619    public int getItemCount() {
620        return super.getItemCount() + (mShowVoicemailPromoCard ? 1 : 0)
621                - (mHiddenPosition != RecyclerView.NO_POSITION ? 1 : 0);
622    }
623
624    @Override
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
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
647    public int getGroupSize(int position) {
648        return super.getGroupSize(position - (mShowVoicemailPromoCard ? 1 : 0));
649    }
650
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
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
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     */
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
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
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     */
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     */
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     */
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     */
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
817    void disableRequestProcessingForTest() {
818        // TODO: Remove this and test the cache directly.
819        mContactInfoCache.disableRequestProcessing();
820    }
821
822    @VisibleForTesting
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
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
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     */
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     */
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     */
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     */
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     */
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