CallLogFragment.java revision 781ee24f91f6eb6a1687d803971196f2bf71c02b
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 android.animation.Animator;
20import android.animation.ValueAnimator;
21import android.animation.Animator.AnimatorListener;
22import android.app.Activity;
23import android.app.KeyguardManager;
24import android.app.ListFragment;
25import android.content.Context;
26import android.content.Intent;
27import android.database.ContentObserver;
28import android.database.Cursor;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.Handler;
32import android.provider.CallLog;
33import android.provider.CallLog.Calls;
34import android.provider.ContactsContract;
35import android.provider.VoicemailContract.Status;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.ViewGroup;
39import android.view.ViewTreeObserver;
40import android.view.View.OnClickListener;
41import android.view.ViewGroup.LayoutParams;
42import android.widget.ListView;
43import android.widget.TextView;
44
45import com.android.common.io.MoreCloseables;
46import com.android.contacts.common.CallUtil;
47import com.android.contacts.common.GeoUtil;
48import com.android.contacts.common.util.PhoneNumberHelper;
49import com.android.contacts.common.util.ViewUtil;
50import com.android.dialer.R;
51import com.android.dialer.list.ListsFragment.HostInterface;
52import com.android.dialer.util.EmptyLoader;
53import com.android.dialer.voicemail.VoicemailStatusHelper;
54import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
55import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
56import com.android.dialerbind.ObjectFactory;
57
58import java.util.List;
59
60/**
61 * Displays a list of call log entries. To filter for a particular kind of call
62 * (all, missed or voicemails), specify it in the constructor.
63 */
64public class CallLogFragment extends ListFragment
65        implements CallLogQueryHandler.Listener,
66        CallLogAdapter.CallFetcher,
67        CallLogAdapter.CallItemExpandedListener {
68    private static final String TAG = "CallLogFragment";
69
70    /**
71     * ID of the empty loader to defer other fragments.
72     */
73    private static final int EMPTY_LOADER_ID = 0;
74
75    private static final String KEY_FILTER_TYPE = "filter_type";
76    private static final String KEY_LOG_LIMIT = "log_limit";
77    private static final String KEY_DATE_LIMIT = "date_limit";
78    private static final String KEY_SHOW_FOOTER = "show_footer";
79
80    private CallLogAdapter mAdapter;
81    private CallLogQueryHandler mCallLogQueryHandler;
82    private boolean mScrollToTop;
83
84    /** Whether there is at least one voicemail source installed. */
85    private boolean mVoicemailSourcesAvailable = false;
86
87    private VoicemailStatusHelper mVoicemailStatusHelper;
88    private View mStatusMessageView;
89    private TextView mStatusMessageText;
90    private TextView mStatusMessageAction;
91    private KeyguardManager mKeyguardManager;
92    private View mFooterView;
93
94    private boolean mEmptyLoaderRunning;
95    private boolean mCallLogFetched;
96    private boolean mVoicemailStatusFetched;
97
98    private float mExpandedItemElevation;
99
100    private final Handler mHandler = new Handler();
101
102    private class CustomContentObserver extends ContentObserver {
103        public CustomContentObserver() {
104            super(mHandler);
105        }
106        @Override
107        public void onChange(boolean selfChange) {
108            mRefreshDataRequired = true;
109        }
110    }
111
112    // See issue 6363009
113    private final ContentObserver mCallLogObserver = new CustomContentObserver();
114    private final ContentObserver mContactsObserver = new CustomContentObserver();
115    private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver();
116    private boolean mRefreshDataRequired = true;
117
118    // Exactly same variable is in Fragment as a package private.
119    private boolean mMenuVisible = true;
120
121    // Default to all calls.
122    private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
123
124    // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler}
125    // will be used.
126    private int mLogLimit = -1;
127
128    // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after
129    // the date filter are included.  If zero, no date-based filtering occurs.
130    private long mDateLimit = 0;
131
132    // Whether or not to show the Show call history footer view
133    private boolean mHasFooterView = false;
134
135    public CallLogFragment() {
136        this(CallLogQueryHandler.CALL_TYPE_ALL, -1);
137    }
138
139    public CallLogFragment(int filterType) {
140        this(filterType, -1);
141    }
142
143    public CallLogFragment(int filterType, int logLimit) {
144        super();
145        mCallTypeFilter = filterType;
146        mLogLimit = logLimit;
147    }
148
149    /**
150     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
151     * after the specified date.
152     * @param filterType type of calls to include.
153     * @param dateLimit limits results to calls occurring on or after the specified date.
154     */
155    public CallLogFragment(int filterType, long dateLimit) {
156        this(filterType, -1, dateLimit);
157    }
158
159    /**
160     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
161     * after the specified date.  Also provides a means to limit the number of results returned.
162     * @param filterType type of calls to include.
163     * @param logLimit limits the number of results to return.
164     * @param dateLimit limits results to calls occurring on or after the specified date.
165     */
166    public CallLogFragment(int filterType, int logLimit, long dateLimit) {
167        this(filterType, logLimit);
168        mDateLimit = dateLimit;
169    }
170
171    @Override
172    public void onCreate(Bundle state) {
173        super.onCreate(state);
174
175        if (state != null) {
176            mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
177            mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
178            mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
179            mHasFooterView = state.getBoolean(KEY_SHOW_FOOTER, mHasFooterView);
180        }
181
182        String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
183        mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this, new ContactInfoHelper(
184                getActivity(), currentCountryIso), this, true);
185        setListAdapter(mAdapter);
186        mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(),
187                this, mLogLimit);
188        mKeyguardManager =
189                (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
190        getActivity().getContentResolver().registerContentObserver(CallLog.CONTENT_URI, true,
191                mCallLogObserver);
192        getActivity().getContentResolver().registerContentObserver(
193                ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver);
194        getActivity().getContentResolver().registerContentObserver(
195                Status.CONTENT_URI, true, mVoicemailStatusObserver);
196        setHasOptionsMenu(true);
197        updateCallList(mCallTypeFilter, mDateLimit);
198
199        mExpandedItemElevation = getResources().getDimension(R.dimen.call_log_expanded_elevation);
200    }
201
202    /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
203    @Override
204    public void onCallsFetched(Cursor cursor) {
205        if (getActivity() == null || getActivity().isFinishing()) {
206            return;
207        }
208        mAdapter.setLoading(false);
209        mAdapter.changeCursor(cursor);
210        // This will update the state of the "Clear call log" menu item.
211        getActivity().invalidateOptionsMenu();
212        if (mScrollToTop) {
213            final ListView listView = getListView();
214            // The smooth-scroll animation happens over a fixed time period.
215            // As a result, if it scrolls through a large portion of the list,
216            // each frame will jump so far from the previous one that the user
217            // will not experience the illusion of downward motion.  Instead,
218            // if we're not already near the top of the list, we instantly jump
219            // near the top, and animate from there.
220            if (listView.getFirstVisiblePosition() > 5) {
221                listView.setSelection(5);
222            }
223            // Workaround for framework issue: the smooth-scroll doesn't
224            // occur if setSelection() is called immediately before.
225            mHandler.post(new Runnable() {
226               @Override
227               public void run() {
228                   if (getActivity() == null || getActivity().isFinishing()) {
229                       return;
230                   }
231                   listView.smoothScrollToPosition(0);
232               }
233            });
234
235            mScrollToTop = false;
236        }
237        mCallLogFetched = true;
238        destroyEmptyLoaderIfAllDataFetched();
239    }
240
241    /**
242     * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
243     */
244    @Override
245    public void onVoicemailStatusFetched(Cursor statusCursor) {
246        if (getActivity() == null || getActivity().isFinishing()) {
247            return;
248        }
249        updateVoicemailStatusMessage(statusCursor);
250
251        int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor);
252        setVoicemailSourcesAvailable(activeSources != 0);
253        MoreCloseables.closeQuietly(statusCursor);
254        mVoicemailStatusFetched = true;
255        destroyEmptyLoaderIfAllDataFetched();
256    }
257
258    private void destroyEmptyLoaderIfAllDataFetched() {
259        if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
260            mEmptyLoaderRunning = false;
261            getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
262        }
263    }
264
265    /** Sets whether there are any voicemail sources available in the platform. */
266    private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) {
267        if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return;
268        mVoicemailSourcesAvailable = voicemailSourcesAvailable;
269
270        Activity activity = getActivity();
271        if (activity != null) {
272            // This is so that the options menu content is updated.
273            activity.invalidateOptionsMenu();
274        }
275    }
276
277    @Override
278    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
279        View view = inflater.inflate(R.layout.call_log_fragment, container, false);
280        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
281        mStatusMessageView = view.findViewById(R.id.voicemail_status);
282        mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
283        mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
284        return view;
285    }
286
287    @Override
288    public void onViewCreated(View view, Bundle savedInstanceState) {
289        super.onViewCreated(view, savedInstanceState);
290        updateEmptyMessage(mCallTypeFilter);
291        getListView().setItemsCanFocus(true);
292        maybeAddFooterView();
293    }
294
295    /**
296     * Based on the new intent, decide whether the list should be configured
297     * to scroll up to display the first item.
298     */
299    public void configureScreenFromIntent(Intent newIntent) {
300        // Typically, when switching to the call-log we want to show the user
301        // the same section of the list that they were most recently looking
302        // at.  However, under some circumstances, we want to automatically
303        // scroll to the top of the list to present the newest call items.
304        // For example, immediately after a call is finished, we want to
305        // display information about that call.
306        mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType());
307    }
308
309    @Override
310    public void onStart() {
311        // Start the empty loader now to defer other fragments.  We destroy it when both calllog
312        // and the voicemail status are fetched.
313        getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
314                new EmptyLoader.Callback(getActivity()));
315        mEmptyLoaderRunning = true;
316        super.onStart();
317    }
318
319    @Override
320    public void onResume() {
321        super.onResume();
322        refreshData();
323    }
324
325    private void updateVoicemailStatusMessage(Cursor statusCursor) {
326        List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
327        if (messages.size() == 0) {
328            mStatusMessageView.setVisibility(View.GONE);
329        } else {
330            mStatusMessageView.setVisibility(View.VISIBLE);
331            // TODO: Change the code to show all messages. For now just pick the first message.
332            final StatusMessage message = messages.get(0);
333            if (message.showInCallLog()) {
334                mStatusMessageText.setText(message.callLogMessageId);
335            }
336            if (message.actionMessageId != -1) {
337                mStatusMessageAction.setText(message.actionMessageId);
338            }
339            if (message.actionUri != null) {
340                mStatusMessageAction.setVisibility(View.VISIBLE);
341                mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
342                    @Override
343                    public void onClick(View v) {
344                        getActivity().startActivity(
345                                new Intent(Intent.ACTION_VIEW, message.actionUri));
346                    }
347                });
348            } else {
349                mStatusMessageAction.setVisibility(View.GONE);
350            }
351        }
352    }
353
354    @Override
355    public void onPause() {
356        super.onPause();
357        // Kill the requests thread
358        mAdapter.stopRequestProcessing();
359    }
360
361    @Override
362    public void onStop() {
363        super.onStop();
364        updateOnExit();
365    }
366
367    @Override
368    public void onDestroy() {
369        super.onDestroy();
370        mAdapter.stopRequestProcessing();
371        mAdapter.changeCursor(null);
372        getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
373        getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
374        getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
375    }
376
377    @Override
378    public void onSaveInstanceState(Bundle outState) {
379        super.onSaveInstanceState(outState);
380        outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
381        outState.putInt(KEY_LOG_LIMIT, mLogLimit);
382        outState.putLong(KEY_DATE_LIMIT, mDateLimit);
383        outState.putBoolean(KEY_SHOW_FOOTER, mHasFooterView);
384    }
385
386    @Override
387    public void fetchCalls() {
388        mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
389    }
390
391    public void startCallsQuery() {
392        mAdapter.setLoading(true);
393        mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
394    }
395
396    private void startVoicemailStatusQuery() {
397        mCallLogQueryHandler.fetchVoicemailStatus();
398    }
399
400    private void updateCallList(int filterType, long dateLimit) {
401        mCallLogQueryHandler.fetchCalls(filterType, dateLimit);
402    }
403
404    private void updateEmptyMessage(int filterType) {
405        final String message;
406        switch (filterType) {
407            case Calls.MISSED_TYPE:
408                message = getString(R.string.recentMissed_empty);
409                break;
410            case Calls.VOICEMAIL_TYPE:
411                message = getString(R.string.recentVoicemails_empty);
412                break;
413            case CallLogQueryHandler.CALL_TYPE_ALL:
414                message = getString(R.string.recentCalls_empty);
415                break;
416            default:
417                throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
418                        + filterType);
419        }
420        ((TextView) getListView().getEmptyView()).setText(message);
421    }
422
423    public void callSelectedEntry() {
424        int position = getListView().getSelectedItemPosition();
425        if (position < 0) {
426            // In touch mode you may often not have something selected, so
427            // just call the first entry to make sure that [send] [send] calls the
428            // most recent entry.
429            position = 0;
430        }
431        final Cursor cursor = (Cursor)mAdapter.getItem(position);
432        if (cursor != null) {
433            String number = cursor.getString(CallLogQuery.NUMBER);
434            int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION);
435            if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) {
436                // This number can't be called, do nothing
437                return;
438            }
439            Intent intent;
440            // If "number" is really a SIP address, construct a sip: URI.
441            if (PhoneNumberHelper.isUriNumber(number)) {
442                intent = CallUtil.getCallIntent(
443                        Uri.fromParts(CallUtil.SCHEME_SIP, number, null));
444            } else {
445                // We're calling a regular PSTN phone number.
446                // Construct a tel: URI, but do some other possible cleanup first.
447                int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
448                if (!number.startsWith("+") &&
449                       (callType == Calls.INCOMING_TYPE
450                                || callType == Calls.MISSED_TYPE)) {
451                    // If the caller-id matches a contact with a better qualified number, use it
452                    String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
453                    number = mAdapter.getBetterNumberFromContacts(number, countryIso);
454                }
455                intent = CallUtil.getCallIntent(
456                        Uri.fromParts(CallUtil.SCHEME_TEL, number, null));
457            }
458            intent.setFlags(
459                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
460            startActivity(intent);
461        }
462    }
463
464    CallLogAdapter getAdapter() {
465        return mAdapter;
466    }
467
468    @Override
469    public void setMenuVisibility(boolean menuVisible) {
470        super.setMenuVisibility(menuVisible);
471        if (mMenuVisible != menuVisible) {
472            mMenuVisible = menuVisible;
473            if (!menuVisible) {
474                updateOnExit();
475            } else if (isResumed()) {
476                refreshData();
477            }
478        }
479    }
480
481    /** Requests updates to the data to be shown. */
482    private void refreshData() {
483        // Prevent unnecessary refresh.
484        if (mRefreshDataRequired) {
485            // Mark all entries in the contact info cache as out of date, so they will be looked up
486            // again once being shown.
487            mAdapter.invalidateCache();
488            startCallsQuery();
489            startVoicemailStatusQuery();
490            updateOnEntry();
491            mRefreshDataRequired = false;
492        }
493    }
494
495    /** Updates call data and notification state while leaving the call log tab. */
496    private void updateOnExit() {
497        updateOnTransition(false);
498    }
499
500    /** Updates call data and notification state while entering the call log tab. */
501    private void updateOnEntry() {
502        updateOnTransition(true);
503    }
504
505    // TODO: Move to CallLogActivity
506    private void updateOnTransition(boolean onEntry) {
507        // We don't want to update any call data when keyguard is on because the user has likely not
508        // seen the new calls yet.
509        // This might be called before onCreate() and thus we need to check null explicitly.
510        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
511            // On either of the transitions we update the missed call and voicemail notifications.
512            // While exiting we additionally consume all missed calls (by marking them as read).
513            mCallLogQueryHandler.markNewCallsAsOld();
514            if (!onEntry) {
515                mCallLogQueryHandler.markMissedCallsAsRead();
516            }
517            CallLogNotificationsHelper.removeMissedCallNotifications();
518            CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
519        }
520    }
521
522    /**
523     * Enables/disables the showing of the view full call history footer
524     *
525     * @param hasFooterView Whether or not to show the footer
526     */
527    public void setHasFooterView(boolean hasFooterView) {
528        mHasFooterView = hasFooterView;
529        maybeAddFooterView();
530    }
531
532    /**
533     * Determine whether or not the footer view should be added to the listview. If getView()
534     * is null, which means onCreateView hasn't been called yet, defer the addition of the footer
535     * until onViewCreated has been called.
536     */
537    private void maybeAddFooterView() {
538        if (!mHasFooterView || getView() == null) {
539            return;
540        }
541
542        if (mFooterView == null) {
543            mFooterView = getActivity().getLayoutInflater().inflate(
544                    R.layout.recents_list_footer, (ViewGroup) getView(), false);
545            mFooterView.setOnClickListener(new OnClickListener() {
546                @Override
547                public void onClick(View v) {
548                    ((HostInterface) getActivity()).showCallHistory();
549                }
550            });
551        }
552
553        final ListView listView = getListView();
554        listView.removeFooterView(mFooterView);
555        listView.addFooterView(mFooterView);
556
557        ViewUtil.addBottomPaddingToListViewForFab(listView, getResources());
558    }
559
560    @Override
561    public void onItemExpanded(final CallLogListItemView view) {
562        final int startingHeight = view.getHeight();
563        final CallLogListItemViews viewHolder = (CallLogListItemViews) view.getTag();
564        final ViewTreeObserver observer = getListView().getViewTreeObserver();
565        observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
566            @Override
567            public boolean onPreDraw() {
568                // We don't want to continue getting called for every draw.
569                if (observer.isAlive()) {
570                    observer.removeOnPreDrawListener(this);
571                }
572                // Calculate some values to help with the animation.
573                final int endingHeight = view.getHeight();
574                final int distance = Math.abs(endingHeight - startingHeight);
575                final int baseHeight = Math.min(endingHeight, startingHeight);
576                final boolean isExpand = endingHeight > startingHeight;
577
578                // Set the views back to the start state of the animation
579                view.getLayoutParams().height = startingHeight;
580                if (!isExpand) {
581                    viewHolder.actionsView.setVisibility(View.VISIBLE);
582                }
583
584                // Set up the fade effect for the action buttons.
585                if (isExpand) {
586                    int fadeDuration = getResources().getInteger(
587                            R.integer.call_log_actions_fade_in_duration);
588                    int startDelay = getResources().getInteger(
589                            R.integer.call_log_actions_fade_start);
590                    // Start the fade in after the expansion has partly completed, otherwise it
591                    // will be mostly over before the expansion completes.
592                    viewHolder.actionsView.setAlpha(0f);
593                    viewHolder.actionsView.animate()
594                            .alpha(1f)
595                            .setStartDelay(startDelay)
596                            .setDuration(fadeDuration)
597                            .start();
598                } else {
599                    int fadeDuration = getResources().getInteger(
600                            R.integer.call_log_actions_fade_out_duration);
601                    viewHolder.actionsView.setAlpha(1f);
602                    viewHolder.actionsView.animate()
603                            .alpha(0f)
604                            .setDuration(fadeDuration)
605                            .start();
606                }
607                view.requestLayout();
608
609                // Set up the animator to animate the expansion and shadow depth.
610                ValueAnimator animator = isExpand ? ValueAnimator.ofFloat(0f, 1f)
611                        : ValueAnimator.ofFloat(1f, 0f);
612
613                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
614                    @Override
615                    public void onAnimationUpdate(ValueAnimator animator) {
616                        Float value = (Float) animator.getAnimatedValue();
617
618                        // For each value from 0 to 1, animate the various parts of the layout.
619                        view.getLayoutParams().height =
620                                (int) (value * distance + baseHeight);
621                        viewHolder.callLogEntryView
622                                .setElevation(mExpandedItemElevation * value);
623                        view.requestLayout();
624                    }
625                });
626                // Set everything to their final values when the animation's done.
627                animator.addListener(new AnimatorListener() {
628                    @Override
629                    public void onAnimationEnd(Animator animation) {
630                        view.getLayoutParams().height = LayoutParams.WRAP_CONTENT;
631
632                        if (!isExpand) {
633                            viewHolder.actionsView.setVisibility(View.GONE);
634                        }
635                    }
636
637                    @Override
638                    public void onAnimationCancel(Animator animation) {}
639                    @Override
640                    public void onAnimationRepeat(Animator animation) { }
641                    @Override
642                    public void onAnimationStart(Animator animation) { }
643                });
644
645                final int expandCollapseDuration = getResources().getInteger(
646                        R.integer.call_log_expand_collapse_duration);
647
648                animator.setDuration(expandCollapseDuration);
649                animator.start();
650
651                // Return false so this draw does not occur to prevent the final frame from
652                // being drawn for the single frame before the animations start.
653                return false;
654            }
655        });
656    }
657
658    /**
659     * Determines whether a call log entry with a given ID is currently visible in the list view.
660     *
661     * @param callId The call ID to check.
662     * @return True if the call log entry with the given ID is visible.
663     */
664    @Override
665    public boolean isItemVisible(long callId) {
666        ListView listView = getListView();
667
668        int firstPosition = listView.getFirstVisiblePosition();
669        int lastPosition = listView.getLastVisiblePosition();
670
671        for (int position = 0; position <= lastPosition - firstPosition; position++) {
672            View view = listView.getChildAt(position);
673
674            if (view != null) {
675                final CallLogListItemViews viewHolder = (CallLogListItemViews) view.getTag();
676                if (viewHolder != null && viewHolder.rowId == callId) {
677                    return true;
678                }
679            }
680        }
681        return false;
682    }
683}
684