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