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