Home | History | Annotate | Download | only in list
      1 /*
      2  * Copyright (C) 2013 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 package com.android.dialer.list;
     17 
     18 import android.animation.Animator;
     19 import android.animation.AnimatorSet;
     20 import android.animation.ObjectAnimator;
     21 import android.app.Activity;
     22 import android.app.Fragment;
     23 import android.app.LoaderManager;
     24 import android.content.Context;
     25 import android.content.CursorLoader;
     26 import android.content.Loader;
     27 import android.content.SharedPreferences;
     28 import android.database.Cursor;
     29 import android.graphics.Rect;
     30 import android.net.Uri;
     31 import android.os.Bundle;
     32 import android.provider.CallLog;
     33 import android.util.Log;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.view.View.OnClickListener;
     37 import android.view.ViewGroup;
     38 import android.view.ViewTreeObserver;
     39 import android.widget.AbsListView;
     40 import android.widget.AdapterView;
     41 import android.widget.AdapterView.OnItemClickListener;
     42 import android.widget.Button;
     43 import android.widget.ImageView;
     44 import android.widget.ListView;
     45 import android.widget.RelativeLayout;
     46 import android.widget.RelativeLayout.LayoutParams;
     47 
     48 import com.android.contacts.common.ContactPhotoManager;
     49 import com.android.contacts.common.ContactTileLoaderFactory;
     50 import com.android.contacts.common.GeoUtil;
     51 import com.android.contacts.common.list.ContactEntry;
     52 import com.android.contacts.common.list.ContactListItemView;
     53 import com.android.contacts.common.list.ContactTileView;
     54 import com.android.dialer.DialtactsActivity;
     55 import com.android.dialer.R;
     56 import com.android.dialer.calllog.CallLogAdapter;
     57 import com.android.dialer.calllog.CallLogQuery;
     58 import com.android.dialer.calllog.CallLogQueryHandler;
     59 import com.android.dialer.calllog.ContactInfoHelper;
     60 import com.android.dialer.list.PhoneFavoritesTileAdapter.ContactTileRow;
     61 import com.android.dialerbind.ObjectFactory;
     62 
     63 import java.util.ArrayList;
     64 import java.util.HashMap;
     65 
     66 /**
     67  * Fragment for Phone UI's favorite screen.
     68  *
     69  * This fragment contains three kinds of contacts in one screen: "starred", "frequent", and "all"
     70  * contacts. To show them at once, this merges results from {@link com.android.contacts.common.list.ContactTileAdapter} and
     71  * {@link com.android.contacts.common.list.PhoneNumberListAdapter} into one unified list using {@link PhoneFavoriteMergedAdapter}.
     72  * A contact filter header is also inserted between those adapters' results.
     73  */
     74 public class PhoneFavoriteFragment extends Fragment implements OnItemClickListener,
     75         CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher,
     76         PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener {
     77 
     78     /**
     79      * By default, the animation code assumes that all items in a list view are of the same height
     80      * when animating new list items into view (e.g. from the bottom of the screen into view).
     81      * This can cause incorrect translation offsets when a item that is larger or smaller than
     82      * other list item is removed from the list. This key is used to provide the actual height
     83      * of the removed object so that the actual translation appears correct to the user.
     84      */
     85     private static final long KEY_REMOVED_ITEM_HEIGHT = Long.MAX_VALUE;
     86 
     87     private static final String TAG = PhoneFavoriteFragment.class.getSimpleName();
     88     private static final boolean DEBUG = false;
     89 
     90     private int mAnimationDuration;
     91 
     92     /**
     93      * Used with LoaderManager.
     94      */
     95     private static int LOADER_ID_CONTACT_TILE = 1;
     96     private static int MISSED_CALL_LOADER = 2;
     97 
     98     private static final String KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE =
     99             "key_last_dismissed_call_shortcut_date";
    100 
    101     public interface OnShowAllContactsListener {
    102         public void onShowAllContacts();
    103     }
    104 
    105     public interface Listener {
    106         public void onContactSelected(Uri contactUri);
    107         public void onCallNumberDirectly(String phoneNumber);
    108     }
    109 
    110     public interface HostInterface {
    111         public void setDragDropController(DragDropController controller);
    112     }
    113 
    114     private class MissedCallLogLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
    115 
    116         @Override
    117         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    118             final Uri uri = CallLog.Calls.CONTENT_URI;
    119             final String[] projection = new String[] {CallLog.Calls.TYPE};
    120             final String selection = CallLog.Calls.TYPE + " = " + CallLog.Calls.MISSED_TYPE +
    121                     " AND " + CallLog.Calls.IS_READ + " = 0";
    122             return new CursorLoader(getActivity(), uri, projection, selection, null, null);
    123         }
    124 
    125         @Override
    126         public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor data) {
    127             mCallLogAdapter.setMissedCalls(data);
    128         }
    129 
    130         @Override
    131         public void onLoaderReset(Loader<Cursor> cursorLoader) {
    132         }
    133     }
    134 
    135     private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
    136         @Override
    137         public CursorLoader onCreateLoader(int id, Bundle args) {
    138             if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader.");
    139             return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
    140         }
    141 
    142         @Override
    143         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    144             if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoadFinished");
    145             mContactTileAdapter.setContactCursor(data);
    146             setEmptyViewVisibility(mContactTileAdapter.getCount() == 0);
    147         }
    148 
    149         @Override
    150         public void onLoaderReset(Loader<Cursor> loader) {
    151             if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. ");
    152         }
    153     }
    154 
    155     private class ContactTileAdapterListener implements ContactTileView.Listener {
    156         @Override
    157         public void onContactSelected(Uri contactUri, Rect targetRect) {
    158             if (mListener != null) {
    159                 mListener.onContactSelected(contactUri);
    160             }
    161         }
    162 
    163         @Override
    164         public void onCallNumberDirectly(String phoneNumber) {
    165             if (mListener != null) {
    166                 mListener.onCallNumberDirectly(phoneNumber);
    167             }
    168         }
    169 
    170         @Override
    171         public int getApproximateTileWidth() {
    172             return getView().getWidth() / mContactTileAdapter.getColumnCount();
    173         }
    174     }
    175 
    176     private class ScrollListener implements ListView.OnScrollListener {
    177         @Override
    178         public void onScroll(AbsListView view,
    179                 int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    180         }
    181 
    182         @Override
    183         public void onScrollStateChanged(AbsListView view, int scrollState) {
    184             mActivityScrollListener.onListFragmentScrollStateChange(scrollState);
    185         }
    186     }
    187 
    188     private Listener mListener;
    189 
    190     private OnListFragmentScrolledListener mActivityScrollListener;
    191     private OnShowAllContactsListener mShowAllContactsListener;
    192     private PhoneFavoriteMergedAdapter mAdapter;
    193     private PhoneFavoritesTileAdapter mContactTileAdapter;
    194 
    195     private CallLogAdapter mCallLogAdapter;
    196     private CallLogQueryHandler mCallLogQueryHandler;
    197 
    198     private View mParentView;
    199 
    200     private PhoneFavoriteListView mListView;
    201 
    202     private View mPhoneFavoritesMenu;
    203     private View mContactTileFrame;
    204 
    205     private TileInteractionTeaserView mTileInteractionTeaserView;
    206 
    207     private final HashMap<Long, Integer> mItemIdTopMap = new HashMap<Long, Integer>();
    208     private final HashMap<Long, Integer> mItemIdLeftMap = new HashMap<Long, Integer>();
    209 
    210     /**
    211      * Layout used when there are no favorites.
    212      */
    213     private View mEmptyView;
    214 
    215     /**
    216      * Call shortcuts older than this date (persisted in shared preferences) will not show up in
    217      * at the top of the screen
    218      */
    219     private long mLastCallShortcutDate = 0;
    220 
    221     /**
    222      * The date of the current call shortcut that is showing on screen.
    223      */
    224     private long mCurrentCallShortcutDate = 0;
    225 
    226     private final ContactTileView.Listener mContactTileAdapterListener =
    227             new ContactTileAdapterListener();
    228     private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener =
    229             new ContactTileLoaderListener();
    230     private final ScrollListener mScrollListener = new ScrollListener();
    231 
    232     @Override
    233     public void onAttach(Activity activity) {
    234         if (DEBUG) Log.d(TAG, "onAttach()");
    235         super.onAttach(activity);
    236 
    237         // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter.
    238         // We don't construct the resultant adapter at this moment since it requires LayoutInflater
    239         // that will be available on onCreateView().
    240         mContactTileAdapter = new PhoneFavoritesTileAdapter(activity, mContactTileAdapterListener,
    241                 this,
    242                 getResources().getInteger(R.integer.contact_tile_column_count_in_favorites),
    243                 PhoneFavoritesTileAdapter.NO_ROW_LIMIT);
    244         mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity));
    245     }
    246 
    247     @Override
    248     public void onCreate(Bundle savedState) {
    249         if (DEBUG) Log.d(TAG, "onCreate()");
    250         super.onCreate(savedState);
    251 
    252         mAnimationDuration = getResources().getInteger(R.integer.fade_duration);
    253         mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(),
    254                 this, 1);
    255         final String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
    256         mCallLogAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this,
    257                 new ContactInfoHelper(getActivity(), currentCountryIso), false, false);
    258         setHasOptionsMenu(true);
    259     }
    260 
    261     @Override
    262     public void onResume() {
    263         super.onResume();
    264         final SharedPreferences prefs = getActivity().getSharedPreferences(
    265                 DialtactsActivity.SHARED_PREFS_NAME, Context.MODE_PRIVATE);
    266 
    267         mLastCallShortcutDate = prefs.getLong(KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE, 0);
    268 
    269         fetchCalls();
    270         mCallLogAdapter.setLoading(true);
    271         getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad();
    272     }
    273 
    274     @Override
    275     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    276             Bundle savedInstanceState) {
    277         mParentView = inflater.inflate(R.layout.phone_favorites_fragment, container, false);
    278 
    279         mListView = (PhoneFavoriteListView) mParentView.findViewById(R.id.contact_tile_list);
    280         mListView.setItemsCanFocus(true);
    281         mListView.setOnItemClickListener(this);
    282         mListView.setVerticalScrollBarEnabled(false);
    283         mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT);
    284         mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
    285         mListView.setOnItemSwipeListener(mContactTileAdapter);
    286         mListView.getDragDropController().addOnDragDropListener(mContactTileAdapter);
    287 
    288         final ImageView dragShadowOverlay =
    289                 (ImageView) mParentView.findViewById(R.id.contact_tile_drag_shadow_overlay);
    290         mListView.setDragShadowOverlay(dragShadowOverlay);
    291 
    292         mEmptyView = mParentView.findViewById(R.id.phone_no_favorites_view);
    293 
    294         mPhoneFavoritesMenu = inflater.inflate(R.layout.phone_favorites_menu, mListView, false);
    295         prepareFavoritesMenu(mPhoneFavoritesMenu);
    296 
    297         mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame);
    298 
    299         mTileInteractionTeaserView = (TileInteractionTeaserView) inflater.inflate(
    300                 R.layout.tile_interactions_teaser_view, mListView, false);
    301 
    302         mAdapter = new PhoneFavoriteMergedAdapter(getActivity(), this, mContactTileAdapter,
    303                 mCallLogAdapter, mPhoneFavoritesMenu, mTileInteractionTeaserView);
    304 
    305         mTileInteractionTeaserView.setAdapter(mAdapter);
    306 
    307         mListView.setAdapter(mAdapter);
    308 
    309         mListView.setOnScrollListener(mScrollListener);
    310         mListView.setFastScrollEnabled(false);
    311         mListView.setFastScrollAlwaysVisible(false);
    312 
    313         return mParentView;
    314     }
    315 
    316     public boolean hasFrequents() {
    317         if (mContactTileAdapter == null) return false;
    318         return mContactTileAdapter.getNumFrequents() > 0;
    319     }
    320 
    321     /* package */ void setEmptyViewVisibility(final boolean visible) {
    322         final int previousVisibility = mEmptyView.getVisibility();
    323         final int newVisibility = visible ? View.VISIBLE : View.GONE;
    324 
    325         if (previousVisibility != newVisibility) {
    326             final RelativeLayout.LayoutParams params = (LayoutParams) mContactTileFrame
    327                     .getLayoutParams();
    328             params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT;
    329             mContactTileFrame.setLayoutParams(params);
    330             mEmptyView.setVisibility(newVisibility);
    331         }
    332     }
    333 
    334     @Override
    335     public void onStart() {
    336         super.onStart();
    337 
    338         final Activity activity = getActivity();
    339 
    340         try {
    341             mActivityScrollListener = (OnListFragmentScrolledListener) activity;
    342         } catch (ClassCastException e) {
    343             throw new ClassCastException(activity.toString()
    344                     + " must implement OnListFragmentScrolledListener");
    345         }
    346 
    347         try {
    348             mShowAllContactsListener = (OnShowAllContactsListener) activity;
    349         } catch (ClassCastException e) {
    350             throw new ClassCastException(activity.toString()
    351                     + " must implement OnShowAllContactsListener");
    352         }
    353 
    354         try {
    355             OnDragDropListener listener = (OnDragDropListener) activity;
    356             mListView.getDragDropController().addOnDragDropListener(listener);
    357             ((HostInterface) activity).setDragDropController(mListView.getDragDropController());
    358         } catch (ClassCastException e) {
    359             throw new ClassCastException(activity.toString()
    360                     + " must implement OnDragDropListener and HostInterface");
    361         }
    362 
    363         // Use initLoader() instead of restartLoader() to refraining unnecessary reload.
    364         // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will
    365         // be called, on which we'll check if "all" contacts should be reloaded again or not.
    366         getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
    367         getLoaderManager().initLoader(MISSED_CALL_LOADER, null, new MissedCallLogLoaderListener());
    368     }
    369 
    370     /**
    371      * {@inheritDoc}
    372      *
    373      * This is only effective for elements provided by {@link #mContactTileAdapter}.
    374      * {@link #mContactTileAdapter} has its own logic for click events.
    375      */
    376     @Override
    377     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    378         final int contactTileAdapterCount = mContactTileAdapter.getCount();
    379         if (position <= contactTileAdapterCount) {
    380             Log.e(TAG, "onItemClick() event for unexpected position. "
    381                     + "The position " + position + " is before \"all\" section. Ignored.");
    382         }
    383     }
    384 
    385     /**
    386      * Gets called when user click on the show all contacts button.
    387      */
    388     private void showAllContacts() {
    389         mShowAllContactsListener.onShowAllContacts();
    390     }
    391 
    392     public void setListener(Listener listener) {
    393         mListener = listener;
    394     }
    395 
    396     @Override
    397     public void onVoicemailStatusFetched(Cursor statusCursor) {
    398         // no-op
    399     }
    400 
    401     @Override
    402     public void onCallsFetched(Cursor cursor) {
    403         animateListView();
    404         mCallLogAdapter.setLoading(false);
    405 
    406         // Save the date of the most recent call log item
    407         if (cursor != null && cursor.moveToFirst()) {
    408             mCurrentCallShortcutDate = cursor.getLong(CallLogQuery.DATE);
    409         }
    410 
    411         mCallLogAdapter.changeCursor(cursor);
    412         mAdapter.notifyDataSetChanged();
    413     }
    414 
    415     @Override
    416     public void fetchCalls() {
    417         mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL, mLastCallShortcutDate);
    418     }
    419 
    420     @Override
    421     public void onPause() {
    422         // If there are any pending contact entries that are to be removed, remove them
    423         mContactTileAdapter.removePendingContactEntry();
    424         // Wipe the cache to refresh the call shortcut item. This is not that expensive because
    425         // it only contains one item.
    426         mCallLogAdapter.invalidateCache();
    427         super.onPause();
    428     }
    429 
    430     /**
    431      * Cache the current view offsets into memory. Once a relayout of views in the ListView
    432      * has happened due to a dataset change, the cached offsets are used to create animations
    433      * that slide views from their previous positions to their new ones, to give the appearance
    434      * that the views are sliding into their new positions.
    435      */
    436     @SuppressWarnings("unchecked")
    437     private void saveOffsets(int removedItemHeight) {
    438         final int firstVisiblePosition = mListView.getFirstVisiblePosition();
    439         if (DEBUG) {
    440             Log.d(TAG, "Child count : " + mListView.getChildCount());
    441         }
    442         for (int i = 0; i < mListView.getChildCount(); i++) {
    443             final View child = mListView.getChildAt(i);
    444             final int position = firstVisiblePosition + i;
    445             final long itemId = mAdapter.getItemId(position);
    446             final int itemViewType = mAdapter.getItemViewType(position);
    447             if (itemViewType == PhoneFavoritesTileAdapter.ViewTypes.TOP &&
    448                     child instanceof ContactTileRow) {
    449                 // This is a tiled row, so save horizontal offsets instead
    450                 saveHorizontalOffsets((ContactTileRow) child, (ArrayList<ContactEntry>)
    451                         mAdapter.getItem(position),
    452                         mAdapter.getAdjustedPositionInContactTileAdapter(position));
    453             }
    454             if (DEBUG) {
    455                 Log.d(TAG, "Saving itemId: " + itemId + " for listview child " + i + " Top: "
    456                         + child.getTop());
    457             }
    458             mItemIdTopMap.put(itemId, child.getTop());
    459         }
    460 
    461         mItemIdTopMap.put(KEY_REMOVED_ITEM_HEIGHT, removedItemHeight);
    462     }
    463 
    464     /**
    465      * Saves the horizontal offsets for contacts that are displayed as tiles in a row. Saving
    466      * these offsets allow us to animate tiles sliding left and right within the same row.
    467      * See {@link #saveOffsets(int removedItemHeight)}
    468      */
    469     private void saveHorizontalOffsets(ContactTileRow row, ArrayList<ContactEntry> list,
    470             int currentRowIndex) {
    471         for (int i = 0; i < list.size() && i < row.getChildCount(); i++) {
    472             final View child = row.getChildAt(i);
    473             if (child == null) {
    474                 continue;
    475             }
    476             final ContactEntry entry = list.get(i);
    477             final long itemId = mContactTileAdapter.getAdjustedItemId(entry.id);
    478             if (DEBUG) {
    479                 Log.d(TAG, "Saving itemId: " + itemId + " for tileview child " + i + " Left: "
    480                         + child.getTop());
    481             }
    482             mItemIdTopMap.put(itemId, currentRowIndex);
    483             mItemIdLeftMap.put(itemId, child.getLeft());
    484         }
    485     }
    486 
    487     /*
    488      * Performs a animations for a row of tiles
    489      */
    490     private void performHorizontalAnimations(ContactTileRow row, ArrayList<ContactEntry> list,
    491             long[] idsInPlace, int currentRow) {
    492         if (mItemIdLeftMap.isEmpty()) {
    493             return;
    494         }
    495         final AnimatorSet animSet = new AnimatorSet();
    496         final ArrayList<Animator> animators = new ArrayList<Animator>();
    497         for (int i = 0; i < list.size(); i++) {
    498             final View child = row.getChildAt(i);
    499             final ContactEntry entry = list.get(i);
    500             final long itemId = mContactTileAdapter.getAdjustedItemId(entry.id);
    501 
    502             if (containsId(idsInPlace, itemId)) {
    503                 animators.add(ObjectAnimator.ofFloat(
    504                         child, "alpha", 0.0f, 1.0f));
    505                 break;
    506             } else {
    507                 Integer startLeft = mItemIdLeftMap.get(itemId);
    508                 int left = child.getLeft();
    509 
    510                 Integer startRow = mItemIdTopMap.get(itemId);
    511                 if (startRow != null) {
    512                     if (startRow > currentRow) {
    513                         // Item has shifted upwards to the previous row.
    514                         // It should now animate in from right to left.
    515                         startLeft = left + child.getWidth();
    516                     } else if (startRow < currentRow) {
    517                         // Item has shifted downwards to the next row.
    518                         // It should now animate in from left to right.
    519                         startLeft = left - child.getWidth();
    520                     }
    521 
    522                     // If the item hasn't shifted rows (startRow == currentRow), it either remains
    523                     // in the same position or has shifted left or right within its current row.
    524                     // Either way, startLeft has already been correctly saved and retrieved from
    525                     // mItemIdTopMap.
    526                 }
    527 
    528                 if (startLeft != null) {
    529                     if (startLeft != left) {
    530                         int delta = startLeft - left;
    531                         if (DEBUG) {
    532                             Log.d(TAG, "Found itemId: " + itemId + " for tileview child " + i +
    533                                     " Left: " + left +
    534                                     " Delta: " + delta);
    535                         }
    536                         animators.add(ObjectAnimator.ofFloat(
    537                                 child, "translationX", delta, 0.0f));
    538                     }
    539                 }
    540             }
    541         }
    542         if (animators.size() > 0) {
    543             animSet.setDuration(mAnimationDuration).playTogether(animators);
    544             animSet.start();
    545         }
    546     }
    547 
    548     /*
    549      * Performs animations for the list view. If the list item is a row of tiles, horizontal
    550      * animations will be performed instead.
    551      */
    552     private void animateListView(final long... idsInPlace) {
    553         if (mItemIdTopMap.isEmpty()) {
    554             // Don't do animations if the database is being queried for the first time and
    555             // the previous item offsets have not been cached, or the user hasn't done anything
    556             // (dragging, swiping etc) that requires an animation.
    557             return;
    558         }
    559 
    560         final int removedItemHeight = mItemIdTopMap.get(KEY_REMOVED_ITEM_HEIGHT);
    561 
    562         final ViewTreeObserver observer = mListView.getViewTreeObserver();
    563         observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    564             @SuppressWarnings("unchecked")
    565             @Override
    566             public boolean onPreDraw() {
    567                 observer.removeOnPreDrawListener(this);
    568                 final int firstVisiblePosition = mListView.getFirstVisiblePosition();
    569                 final AnimatorSet animSet = new AnimatorSet();
    570                 final ArrayList<Animator> animators = new ArrayList<Animator>();
    571                 for (int i = 0; i < mListView.getChildCount(); i++) {
    572                     final View child = mListView.getChildAt(i);
    573                     int position = firstVisiblePosition + i;
    574                     final int itemViewType = mAdapter.getItemViewType(position);
    575                     if (itemViewType == PhoneFavoritesTileAdapter.ViewTypes.TOP &&
    576                             child instanceof ContactTileRow) {
    577                         // This is a tiled row, so perform horizontal animations instead
    578                         performHorizontalAnimations((ContactTileRow) child, (
    579                                 ArrayList<ContactEntry>) mAdapter.getItem(position), idsInPlace,
    580                                 mAdapter.getAdjustedPositionInContactTileAdapter(position));
    581                     }
    582 
    583                     final long itemId = mAdapter.getItemId(position);
    584 
    585                     if (containsId(idsInPlace, itemId)) {
    586                         animators.add(ObjectAnimator.ofFloat(
    587                                 child, "alpha", 0.0f, 1.0f));
    588                         break;
    589                     } else {
    590                         Integer startTop = mItemIdTopMap.get(itemId);
    591                         final int top = child.getTop();
    592                         int delta = 0;
    593                         if (startTop != null) {
    594                             if (startTop != top) {
    595                                 delta = startTop - top;
    596                             }
    597                         } else if (!mItemIdLeftMap.containsKey(itemId)) {
    598                             // Animate new views along with the others. The catch is that they did
    599                             // not exist in the start state, so we must calculate their starting
    600                             // position based on neighboring views.
    601 
    602                             final int itemHeight;
    603                             if (removedItemHeight == 0) {
    604                                 itemHeight = child.getHeight() + mListView.getDividerHeight();
    605                             } else {
    606                                 itemHeight = removedItemHeight;
    607                             }
    608                             startTop = top + (i > 0 ? itemHeight : -itemHeight);
    609                             delta = startTop - top;
    610                         }
    611                         if (DEBUG) {
    612                             Log.d(TAG, "Found itemId: " + itemId + " for listview child " + i +
    613                                     " Top: " + top +
    614                                     " Delta: " + delta);
    615                         }
    616 
    617                         if (delta != 0) {
    618                             animators.add(ObjectAnimator.ofFloat(
    619                                     child, "translationY", delta, 0.0f));
    620                         }
    621                     }
    622                 }
    623 
    624                 if (animators.size() > 0) {
    625                     animSet.setDuration(mAnimationDuration).playTogether(animators);
    626                     animSet.start();
    627                 }
    628 
    629                 mItemIdTopMap.clear();
    630                 mItemIdLeftMap.clear();
    631                 return true;
    632             }
    633         });
    634     }
    635 
    636     private boolean containsId(long[] ids, long target) {
    637         // Linear search on array is fine because this is typically only 0-1 elements long
    638         for (int i = 0; i < ids.length; i++) {
    639             if (ids[i] == target) {
    640                 return true;
    641             }
    642         }
    643         return false;
    644     }
    645 
    646     @Override
    647     public void onDataSetChangedForAnimation(long... idsInPlace) {
    648         animateListView(idsInPlace);
    649     }
    650 
    651     @Override
    652     public void cacheOffsetsForDatasetChange() {
    653         saveOffsets(0);
    654     }
    655 
    656     public void dismissShortcut(int height) {
    657         saveOffsets(height);
    658         mLastCallShortcutDate = mCurrentCallShortcutDate;
    659         final SharedPreferences prefs = getActivity().getSharedPreferences(
    660                 DialtactsActivity.SHARED_PREFS_NAME, Context.MODE_PRIVATE);
    661         prefs.edit().putLong(KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE, mLastCallShortcutDate)
    662                 .apply();
    663         fetchCalls();
    664     }
    665 
    666     /**
    667      * Prepares the favorites menu which contains the static label "Speed Dial" and the
    668      * "All Contacts" button.  Sets the onClickListener for the "All Contacts" button.
    669      */
    670     private void prepareFavoritesMenu(View favoritesMenu) {
    671         Button allContactsButton = (Button) favoritesMenu.findViewById(R.id.all_contacts_button);
    672         // Set the onClick listener for the button to bring up the all contacts view.
    673         allContactsButton.setOnClickListener(new OnClickListener() {
    674             @Override
    675             public void onClick(View view) {
    676                 showAllContacts();
    677             }
    678         });
    679     }
    680 }
    681