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