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 {
CustomContentObserver()112         public CustomContentObserver() {
113             super(mHandler);
114         }
115         @Override
onChange(boolean selfChange)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 
CallLogFragment()144     public CallLogFragment() {
145         this(CallLogQueryHandler.CALL_TYPE_ALL, -1);
146     }
147 
CallLogFragment(int filterType)148     public CallLogFragment(int filterType) {
149         this(filterType, -1);
150     }
151 
CallLogFragment(int filterType, int logLimit)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      */
CallLogFragment(int filterType, long dateLimit)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      */
CallLogFragment(int filterType, int logLimit, long dateLimit)175     public CallLogFragment(int filterType, int logLimit, long dateLimit) {
176         this(filterType, logLimit);
177         mDateLimit = dateLimit;
178     }
179 
180     @Override
onCreate(Bundle state)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
onCallsFetched(Cursor cursor)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
onVoicemailStatusFetched(Cursor statusCursor)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 
destroyEmptyLoaderIfAllDataFetched()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. */
setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)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
onViewCreated(View view, Bundle savedInstanceState)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      */
configureScreenFromIntent(Intent newIntent)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
onStart()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
onResume()348     public void onResume() {
349         super.onResume();
350         refreshData();
351     }
352 
updateVoicemailStatusMessage(Cursor statusCursor)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
onPause()383     public void onPause() {
384         super.onPause();
385         // Kill the requests thread
386         mAdapter.stopRequestProcessing();
387     }
388 
389     @Override
onStop()390     public void onStop() {
391         super.onStop();
392         updateOnExit();
393     }
394 
395     @Override
onDestroy()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
onSaveInstanceState(Bundle outState)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
fetchCalls()417     public void fetchCalls() {
418         mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
419     }
420 
startCallsQuery()421     public void startCallsQuery() {
422         mAdapter.setLoading(true);
423         mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
424     }
425 
startVoicemailStatusQuery()426     private void startVoicemailStatusQuery() {
427         mCallLogQueryHandler.fetchVoicemailStatus();
428     }
429 
updateCallList(int filterType, long dateLimit)430     private void updateCallList(int filterType, long dateLimit) {
431         mCallLogQueryHandler.fetchCalls(filterType, dateLimit);
432     }
433 
updateEmptyMessage(int filterType)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 
getAdapter()454     CallLogAdapter getAdapter() {
455         return mAdapter;
456     }
457 
458     @Override
setMenuVisibility(boolean menuVisible)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. */
refreshData()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. */
updateOnExit()486     private void updateOnExit() {
487         updateOnTransition(false);
488     }
489 
490     /** Updates call data and notification state while entering the call log tab. */
updateOnEntry()491     private void updateOnEntry() {
492         updateOnTransition(true);
493     }
494 
495     // TODO: Move to CallLogActivity
updateOnTransition(boolean onEntry)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      */
setHasFooterView(boolean hasFooterView)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      */
maybeAddFooterView()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
onItemExpanded(final View view)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
getViewForCallId(long callId)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 
onBadDataReported(String number)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 
onReportButtonClick(String number)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