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