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