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