CallLogFragment.java revision c44a26cd4de71485047ab3b6c68c9250c02bb27a
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(getActivity()); 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