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