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