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 static android.Manifest.permission.READ_CONTACTS;
     19 
     20 import android.animation.Animator;
     21 import android.animation.AnimatorSet;
     22 import android.animation.ObjectAnimator;
     23 import android.app.Activity;
     24 import android.app.Fragment;
     25 import android.app.LoaderManager;
     26 import android.content.CursorLoader;
     27 import android.content.Loader;
     28 import android.content.pm.PackageManager;
     29 import android.database.Cursor;
     30 import android.graphics.Rect;
     31 import android.net.Uri;
     32 import android.os.Bundle;
     33 import android.os.Trace;
     34 import android.util.Log;
     35 import android.view.LayoutInflater;
     36 import android.view.View;
     37 import android.view.ViewGroup;
     38 import android.view.ViewTreeObserver;
     39 import android.view.animation.AnimationUtils;
     40 import android.view.animation.LayoutAnimationController;
     41 import android.widget.AbsListView;
     42 import android.widget.AdapterView;
     43 import android.widget.AdapterView.OnItemClickListener;
     44 import android.widget.FrameLayout;
     45 import android.widget.FrameLayout.LayoutParams;
     46 import android.widget.ImageView;
     47 import android.widget.ListView;
     48 
     49 import com.android.contacts.common.ContactPhotoManager;
     50 import com.android.contacts.common.ContactTileLoaderFactory;
     51 import com.android.contacts.common.list.ContactTileView;
     52 import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
     53 import com.android.contacts.common.util.PermissionsUtil;
     54 import com.android.dialer.R;
     55 import com.android.dialer.util.DialerUtils;
     56 import com.android.dialer.widget.EmptyContentView;
     57 
     58 import java.util.ArrayList;
     59 import java.util.HashMap;
     60 
     61 /**
     62  * This fragment displays the user's favorite/frequent contacts in a grid.
     63  */
     64 public class SpeedDialFragment extends Fragment implements OnItemClickListener,
     65         PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener,
     66         EmptyContentView.OnEmptyViewActionButtonClickedListener {
     67 
     68     private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
     69 
     70     /**
     71      * By default, the animation code assumes that all items in a list view are of the same height
     72      * when animating new list items into view (e.g. from the bottom of the screen into view).
     73      * This can cause incorrect translation offsets when a item that is larger or smaller than
     74      * other list item is removed from the list. This key is used to provide the actual height
     75      * of the removed object so that the actual translation appears correct to the user.
     76      */
     77     private static final long KEY_REMOVED_ITEM_HEIGHT = Long.MAX_VALUE;
     78 
     79     private static final String TAG = "SpeedDialFragment";
     80     private static final boolean DEBUG = false;
     81 
     82     private int mAnimationDuration;
     83 
     84     /**
     85      * Used with LoaderManager.
     86      */
     87     private static int LOADER_ID_CONTACT_TILE = 1;
     88 
     89     public interface HostInterface {
     90         public void setDragDropController(DragDropController controller);
     91         public void showAllContactsTab();
     92     }
     93 
     94     private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
     95         @Override
     96         public CursorLoader onCreateLoader(int id, Bundle args) {
     97             if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader.");
     98             return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
     99         }
    100 
    101         @Override
    102         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    103             if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoadFinished");
    104             mContactTileAdapter.setContactCursor(data);
    105             setEmptyViewVisibility(mContactTileAdapter.getCount() == 0);
    106         }
    107 
    108         @Override
    109         public void onLoaderReset(Loader<Cursor> loader) {
    110             if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. ");
    111         }
    112     }
    113 
    114     private class ContactTileAdapterListener implements ContactTileView.Listener {
    115         @Override
    116         public void onContactSelected(Uri contactUri, Rect targetRect) {
    117             if (mPhoneNumberPickerActionListener != null) {
    118                 mPhoneNumberPickerActionListener.onPickPhoneNumberAction(contactUri);
    119             }
    120         }
    121 
    122         @Override
    123         public void onCallNumberDirectly(String phoneNumber) {
    124             if (mPhoneNumberPickerActionListener != null) {
    125                 mPhoneNumberPickerActionListener.onCallNumberDirectly(phoneNumber);
    126             }
    127         }
    128 
    129         @Override
    130         public int getApproximateTileWidth() {
    131             return getView().getWidth();
    132         }
    133     }
    134 
    135     private class ScrollListener implements ListView.OnScrollListener {
    136         @Override
    137         public void onScroll(AbsListView view,
    138                 int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    139             if (mActivityScrollListener != null) {
    140                 mActivityScrollListener.onListFragmentScroll(firstVisibleItem, visibleItemCount,
    141                     totalItemCount);
    142             }
    143         }
    144 
    145         @Override
    146         public void onScrollStateChanged(AbsListView view, int scrollState) {
    147             mActivityScrollListener.onListFragmentScrollStateChange(scrollState);
    148         }
    149     }
    150 
    151     private OnPhoneNumberPickerActionListener mPhoneNumberPickerActionListener;
    152 
    153     private OnListFragmentScrolledListener mActivityScrollListener;
    154     private PhoneFavoritesTileAdapter mContactTileAdapter;
    155 
    156     private View mParentView;
    157 
    158     private PhoneFavoriteListView mListView;
    159 
    160     private View mContactTileFrame;
    161 
    162     private final HashMap<Long, Integer> mItemIdTopMap = new HashMap<Long, Integer>();
    163     private final HashMap<Long, Integer> mItemIdLeftMap = new HashMap<Long, Integer>();
    164 
    165     /**
    166      * Layout used when there are no favorites.
    167      */
    168     private EmptyContentView mEmptyView;
    169 
    170     private final ContactTileView.Listener mContactTileAdapterListener =
    171             new ContactTileAdapterListener();
    172     private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener =
    173             new ContactTileLoaderListener();
    174     private final ScrollListener mScrollListener = new ScrollListener();
    175 
    176     @Override
    177     public void onAttach(Activity activity) {
    178         if (DEBUG) Log.d(TAG, "onAttach()");
    179         super.onAttach(activity);
    180 
    181         // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter.
    182         // We don't construct the resultant adapter at this moment since it requires LayoutInflater
    183         // that will be available on onCreateView().
    184         mContactTileAdapter = new PhoneFavoritesTileAdapter(activity, mContactTileAdapterListener,
    185                 this);
    186         mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity));
    187     }
    188 
    189     @Override
    190     public void onCreate(Bundle savedState) {
    191         if (DEBUG) Log.d(TAG, "onCreate()");
    192         Trace.beginSection(TAG + " onCreate");
    193         super.onCreate(savedState);
    194 
    195         mAnimationDuration = getResources().getInteger(R.integer.fade_duration);
    196         Trace.endSection();
    197     }
    198 
    199     @Override
    200     public void onResume() {
    201         Trace.beginSection(TAG + " onResume");
    202         super.onResume();
    203 
    204         if (PermissionsUtil.hasContactsPermissions(getActivity())) {
    205             if (getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE) == null) {
    206                 getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null,
    207                         mContactTileLoaderListener);
    208 
    209             } else {
    210                 getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad();
    211             }
    212 
    213             mEmptyView.setDescription(R.string.speed_dial_empty);
    214             mEmptyView.setActionLabel(R.string.speed_dial_empty_add_favorite_action);
    215         } else {
    216             mEmptyView.setDescription(R.string.permission_no_speeddial);
    217             mEmptyView.setActionLabel(R.string.permission_single_turn_on);
    218         }
    219         Trace.endSection();
    220     }
    221 
    222     @Override
    223     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    224             Bundle savedInstanceState) {
    225         Trace.beginSection(TAG + " onCreateView");
    226         mParentView = inflater.inflate(R.layout.speed_dial_fragment, container, false);
    227 
    228         mListView = (PhoneFavoriteListView) mParentView.findViewById(R.id.contact_tile_list);
    229         mListView.setOnItemClickListener(this);
    230         mListView.setVerticalScrollBarEnabled(false);
    231         mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT);
    232         mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
    233         mListView.getDragDropController().addOnDragDropListener(mContactTileAdapter);
    234 
    235         final ImageView dragShadowOverlay =
    236                 (ImageView) getActivity().findViewById(R.id.contact_tile_drag_shadow_overlay);
    237         mListView.setDragShadowOverlay(dragShadowOverlay);
    238 
    239         mEmptyView = (EmptyContentView) mParentView.findViewById(R.id.empty_list_view);
    240         mEmptyView.setImage(R.drawable.empty_speed_dial);
    241         mEmptyView.setActionClickedListener(this);
    242 
    243         mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame);
    244 
    245         final LayoutAnimationController controller = new LayoutAnimationController(
    246                 AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in));
    247         controller.setDelay(0);
    248         mListView.setLayoutAnimation(controller);
    249         mListView.setAdapter(mContactTileAdapter);
    250 
    251         mListView.setOnScrollListener(mScrollListener);
    252         mListView.setFastScrollEnabled(false);
    253         mListView.setFastScrollAlwaysVisible(false);
    254         Trace.endSection();
    255         return mParentView;
    256     }
    257 
    258     public boolean hasFrequents() {
    259         if (mContactTileAdapter == null) return false;
    260         return mContactTileAdapter.getNumFrequents() > 0;
    261     }
    262 
    263     /* package */ void setEmptyViewVisibility(final boolean visible) {
    264         final int previousVisibility = mEmptyView.getVisibility();
    265         final int emptyViewVisibility = visible ? View.VISIBLE : View.GONE;
    266         final int listViewVisibility = visible ? View.GONE : View.VISIBLE;
    267 
    268         if (previousVisibility != emptyViewVisibility) {
    269             final FrameLayout.LayoutParams params = (LayoutParams) mContactTileFrame
    270                     .getLayoutParams();
    271             params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT;
    272             mContactTileFrame.setLayoutParams(params);
    273             mEmptyView.setVisibility(emptyViewVisibility);
    274             mListView.setVisibility(listViewVisibility);
    275         }
    276     }
    277 
    278     @Override
    279     public void onStart() {
    280         super.onStart();
    281 
    282         final Activity activity = getActivity();
    283 
    284         try {
    285             mActivityScrollListener = (OnListFragmentScrolledListener) activity;
    286         } catch (ClassCastException e) {
    287             throw new ClassCastException(activity.toString()
    288                     + " must implement OnListFragmentScrolledListener");
    289         }
    290 
    291         try {
    292             OnDragDropListener listener = (OnDragDropListener) activity;
    293             mListView.getDragDropController().addOnDragDropListener(listener);
    294             ((HostInterface) activity).setDragDropController(mListView.getDragDropController());
    295         } catch (ClassCastException e) {
    296             throw new ClassCastException(activity.toString()
    297                     + " must implement OnDragDropListener and HostInterface");
    298         }
    299 
    300         try {
    301             mPhoneNumberPickerActionListener = (OnPhoneNumberPickerActionListener) activity;
    302         } catch (ClassCastException e) {
    303             throw new ClassCastException(activity.toString()
    304                     + " must implement PhoneFavoritesFragment.listener");
    305         }
    306 
    307         // Use initLoader() instead of restartLoader() to refraining unnecessary reload.
    308         // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will
    309         // be called, on which we'll check if "all" contacts should be reloaded again or not.
    310         if (PermissionsUtil.hasContactsPermissions(activity)) {
    311             getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
    312         } else {
    313             setEmptyViewVisibility(true);
    314         }
    315     }
    316 
    317     /**
    318      * {@inheritDoc}
    319      *
    320      * This is only effective for elements provided by {@link #mContactTileAdapter}.
    321      * {@link #mContactTileAdapter} has its own logic for click events.
    322      */
    323     @Override
    324     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    325         final int contactTileAdapterCount = mContactTileAdapter.getCount();
    326         if (position <= contactTileAdapterCount) {
    327             Log.e(TAG, "onItemClick() event for unexpected position. "
    328                     + "The position " + position + " is before \"all\" section. Ignored.");
    329         }
    330     }
    331 
    332     /**
    333      * Cache the current view offsets into memory. Once a relayout of views in the ListView
    334      * has happened due to a dataset change, the cached offsets are used to create animations
    335      * that slide views from their previous positions to their new ones, to give the appearance
    336      * that the views are sliding into their new positions.
    337      */
    338     private void saveOffsets(int removedItemHeight) {
    339         final int firstVisiblePosition = mListView.getFirstVisiblePosition();
    340         if (DEBUG) {
    341             Log.d(TAG, "Child count : " + mListView.getChildCount());
    342         }
    343         for (int i = 0; i < mListView.getChildCount(); i++) {
    344             final View child = mListView.getChildAt(i);
    345             final int position = firstVisiblePosition + i;
    346             // Since we are getting the position from mListView and then querying
    347             // mContactTileAdapter, its very possible that things are out of sync
    348             // and we might index out of bounds.  Let's make sure that this doesn't happen.
    349             if (!mContactTileAdapter.isIndexInBound(position)) {
    350                 continue;
    351             }
    352             final long itemId = mContactTileAdapter.getItemId(position);
    353             if (DEBUG) {
    354                 Log.d(TAG, "Saving itemId: " + itemId + " for listview child " + i + " Top: "
    355                         + child.getTop());
    356             }
    357             mItemIdTopMap.put(itemId, child.getTop());
    358             mItemIdLeftMap.put(itemId, child.getLeft());
    359         }
    360         mItemIdTopMap.put(KEY_REMOVED_ITEM_HEIGHT, removedItemHeight);
    361     }
    362 
    363     /*
    364      * Performs animations for the gridView
    365      */
    366     private void animateGridView(final long... idsInPlace) {
    367         if (mItemIdTopMap.isEmpty()) {
    368             // Don't do animations if the database is being queried for the first time and
    369             // the previous item offsets have not been cached, or the user hasn't done anything
    370             // (dragging, swiping etc) that requires an animation.
    371             return;
    372         }
    373 
    374         final ViewTreeObserver observer = mListView.getViewTreeObserver();
    375         observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    376             @SuppressWarnings("unchecked")
    377             @Override
    378             public boolean onPreDraw() {
    379                 observer.removeOnPreDrawListener(this);
    380                 final int firstVisiblePosition = mListView.getFirstVisiblePosition();
    381                 final AnimatorSet animSet = new AnimatorSet();
    382                 final ArrayList<Animator> animators = new ArrayList<Animator>();
    383                 for (int i = 0; i < mListView.getChildCount(); i++) {
    384                     final View child = mListView.getChildAt(i);
    385                     int position = firstVisiblePosition + i;
    386 
    387                     // Since we are getting the position from mListView and then querying
    388                     // mContactTileAdapter, its very possible that things are out of sync
    389                     // and we might index out of bounds.  Let's make sure that this doesn't happen.
    390                     if (!mContactTileAdapter.isIndexInBound(position)) {
    391                         continue;
    392                     }
    393 
    394                     final long itemId = mContactTileAdapter.getItemId(position);
    395 
    396                     if (containsId(idsInPlace, itemId)) {
    397                         animators.add(ObjectAnimator.ofFloat(
    398                                 child, "alpha", 0.0f, 1.0f));
    399                         break;
    400                     } else {
    401                         Integer startTop = mItemIdTopMap.get(itemId);
    402                         Integer startLeft = mItemIdLeftMap.get(itemId);
    403                         final int top = child.getTop();
    404                         final int left = child.getLeft();
    405                         int deltaX = 0;
    406                         int deltaY = 0;
    407 
    408                         if (startLeft != null) {
    409                             if (startLeft != left) {
    410                                 deltaX = startLeft - left;
    411                                 animators.add(ObjectAnimator.ofFloat(
    412                                         child, "translationX", deltaX, 0.0f));
    413                             }
    414                         }
    415 
    416                         if (startTop != null) {
    417                             if (startTop != top) {
    418                                 deltaY = startTop - top;
    419                                 animators.add(ObjectAnimator.ofFloat(
    420                                         child, "translationY", deltaY, 0.0f));
    421                             }
    422                         }
    423 
    424                         if (DEBUG) {
    425                             Log.d(TAG, "Found itemId: " + itemId + " for listview child " + i +
    426                                     " Top: " + top +
    427                                     " Delta: " + deltaY);
    428                         }
    429                     }
    430                 }
    431 
    432                 if (animators.size() > 0) {
    433                     animSet.setDuration(mAnimationDuration).playTogether(animators);
    434                     animSet.start();
    435                 }
    436 
    437                 mItemIdTopMap.clear();
    438                 mItemIdLeftMap.clear();
    439                 return true;
    440             }
    441         });
    442     }
    443 
    444     private boolean containsId(long[] ids, long target) {
    445         // Linear search on array is fine because this is typically only 0-1 elements long
    446         for (int i = 0; i < ids.length; i++) {
    447             if (ids[i] == target) {
    448                 return true;
    449             }
    450         }
    451         return false;
    452     }
    453 
    454     @Override
    455     public void onDataSetChangedForAnimation(long... idsInPlace) {
    456         animateGridView(idsInPlace);
    457     }
    458 
    459     @Override
    460     public void cacheOffsetsForDatasetChange() {
    461         saveOffsets(0);
    462     }
    463 
    464     public AbsListView getListView() {
    465         return mListView;
    466     }
    467 
    468     @Override
    469     public void onEmptyViewActionButtonClicked() {
    470         final Activity activity = getActivity();
    471         if (activity == null) {
    472             return;
    473         }
    474 
    475         if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) {
    476             requestPermissions(new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE);
    477         } else {
    478             // Switch tabs
    479             ((HostInterface) activity).showAllContactsTab();
    480         }
    481     }
    482 
    483     @Override
    484     public void onRequestPermissionsResult(int requestCode, String[] permissions,
    485             int[] grantResults) {
    486         if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) {
    487             if (grantResults.length == 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
    488                 PermissionsUtil.notifyPermissionGranted(getActivity(), READ_CONTACTS);
    489             }
    490         }
    491     }
    492 }
    493