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.app.widget.EmptyContentView;
54 import com.android.dialer.callintent.CallInitiationType;
55 import com.android.dialer.callintent.CallSpecificAppData;
56 import com.android.dialer.common.LogUtil;
57 import com.android.dialer.util.PermissionsUtil;
58 import com.android.dialer.util.ViewUtil;
59 import java.util.ArrayList;
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
onCreate(Bundle savedState)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
onResume()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
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 
hasFrequents()185   public boolean hasFrequents() {
186     if (mContactTileAdapter == null) {
187       return false;
188     }
189     return mContactTileAdapter.getNumFrequents() > 0;
190   }
191 
setEmptyViewVisibility(final boolean visible)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
onStart()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
onItemClick(AdapterView<?> parent, View view, int position, long id)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    */
saveOffsets(int removedItemHeight)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    */
animateGridView(final long... idsInPlace)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 
containsId(long[] ids, long target)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
onDataSetChangedForAnimation(long... idsInPlace)391   public void onDataSetChangedForAnimation(long... idsInPlace) {
392     animateGridView(idsInPlace);
393   }
394 
395   @Override
cacheOffsetsForDatasetChange()396   public void cacheOffsetsForDatasetChange() {
397     saveOffsets(0);
398   }
399 
400   @Override
onEmptyViewActionButtonClicked()401   public void onEmptyViewActionButtonClicked() {
402     final Activity activity = getActivity();
403     if (activity == null) {
404       return;
405     }
406 
407     if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) {
408       FragmentCompat.requestPermissions(
409           this, new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE);
410     } else {
411       // Switch tabs
412       ((HostInterface) activity).showAllContactsTab();
413     }
414   }
415 
416   @Override
onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults)417   public void onRequestPermissionsResult(
418       int requestCode, String[] permissions, int[] grantResults) {
419     if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) {
420       if (grantResults.length == 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
421         PermissionsUtil.notifyPermissionGranted(getActivity(), READ_CONTACTS);
422       }
423     }
424   }
425 
426   public interface HostInterface {
427 
setDragDropController(DragDropController controller)428     void setDragDropController(DragDropController controller);
429 
showAllContactsTab()430     void showAllContactsTab();
431   }
432 
433   private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
434 
435     @Override
onCreateLoader(int id, Bundle args)436     public CursorLoader onCreateLoader(int id, Bundle args) {
437       if (DEBUG) {
438         LogUtil.d("ContactTileLoaderListener.onCreateLoader", null);
439       }
440       return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
441     }
442 
443     @Override
onLoadFinished(Loader<Cursor> loader, Cursor data)444     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
445       if (DEBUG) {
446         LogUtil.d("ContactTileLoaderListener.onLoadFinished", null);
447       }
448       mContactTileAdapter.setContactCursor(data);
449       setEmptyViewVisibility(mContactTileAdapter.getCount() == 0);
450     }
451 
452     @Override
onLoaderReset(Loader<Cursor> loader)453     public void onLoaderReset(Loader<Cursor> loader) {
454       if (DEBUG) {
455         LogUtil.d("ContactTileLoaderListener.onLoaderReset", null);
456       }
457     }
458   }
459 
460   private class ContactTileAdapterListener implements ContactTileView.Listener {
461 
462     @Override
onContactSelected(Uri contactUri, Rect targetRect)463     public void onContactSelected(Uri contactUri, Rect targetRect) {
464       if (mPhoneNumberPickerActionListener != null) {
465         CallSpecificAppData callSpecificAppData =
466             CallSpecificAppData.newBuilder()
467                 .setCallInitiationType(CallInitiationType.Type.SPEED_DIAL)
468                 .build();
469         mPhoneNumberPickerActionListener.onPickDataUri(
470             contactUri, false /* isVideoCall */, callSpecificAppData);
471       }
472     }
473 
474     @Override
onCallNumberDirectly(String phoneNumber)475     public void onCallNumberDirectly(String phoneNumber) {
476       if (mPhoneNumberPickerActionListener != null) {
477         CallSpecificAppData callSpecificAppData =
478             CallSpecificAppData.newBuilder()
479                 .setCallInitiationType(CallInitiationType.Type.SPEED_DIAL)
480                 .build();
481         mPhoneNumberPickerActionListener.onPickPhoneNumber(
482             phoneNumber, false /* isVideoCall */, callSpecificAppData);
483       }
484     }
485   }
486 
487   private class ScrollListener implements ListView.OnScrollListener {
488 
489     @Override
onScroll( AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)490     public void onScroll(
491         AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
492       if (mActivityScrollListener != null) {
493         mActivityScrollListener.onListFragmentScroll(
494             firstVisibleItem, visibleItemCount, totalItemCount);
495       }
496     }
497 
498     @Override
onScrollStateChanged(AbsListView view, int scrollState)499     public void onScrollStateChanged(AbsListView view, int scrollState) {
500       mActivityScrollListener.onListFragmentScrollStateChange(scrollState);
501     }
502   }
503 }
504