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