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