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