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