1 package com.android.dialer.list;
2 
3 import android.animation.LayoutTransition;
4 import android.app.ActionBar;
5 import android.app.Fragment;
6 import android.app.FragmentManager;
7 import android.content.Context;
8 import android.content.SharedPreferences;
9 import android.database.Cursor;
10 import android.os.Bundle;
11 import android.support.v13.app.FragmentPagerAdapter;
12 import android.support.v4.view.ViewPager;
13 import android.support.v4.view.ViewPager.OnPageChangeListener;
14 import android.util.Log;
15 import android.view.LayoutInflater;
16 import android.view.View;
17 import android.view.ViewGroup;
18 import android.widget.AbsListView;
19 import android.widget.ListView;
20 
21 import com.android.contacts.common.GeoUtil;
22 import com.android.contacts.common.list.ViewPagerTabs;
23 import com.android.contacts.commonbind.analytics.AnalyticsUtil;
24 import com.android.dialer.DialtactsActivity;
25 import com.android.dialer.R;
26 import com.android.dialer.calllog.CallLogAdapter;
27 import com.android.dialer.calllog.CallLogFragment;
28 import com.android.dialer.calllog.CallLogQuery;
29 import com.android.dialer.calllog.CallLogQueryHandler;
30 import com.android.dialer.calllog.ContactInfoHelper;
31 import com.android.dialer.list.ShortcutCardsAdapter.SwipeableShortcutCard;
32 import com.android.dialer.util.DialerUtils;
33 import com.android.dialer.widget.ActionBarController;
34 import com.android.dialer.widget.OverlappingPaneLayout;
35 import com.android.dialer.widget.OverlappingPaneLayout.PanelSlideCallbacks;
36 import com.android.dialerbind.ObjectFactory;
37 
38 import java.util.ArrayList;
39 
40 /**
41  * Fragment that is used as the main screen of the Dialer.
42  *
43  * Contains a ViewPager that contains various contact lists like the Speed Dial list and the
44  * All Contacts list. This will also eventually contain the logic that allows sliding the
45  * ViewPager containing the lists up above the shortcut cards and pin it against the top of the
46  * screen.
47  */
48 public class ListsFragment extends Fragment implements CallLogQueryHandler.Listener,
49         CallLogAdapter.CallFetcher, ViewPager.OnPageChangeListener {
50 
51     private static final boolean DEBUG = DialtactsActivity.DEBUG;
52     private static final String TAG = "ListsFragment";
53 
54     public static final int TAB_INDEX_SPEED_DIAL = 0;
55     public static final int TAB_INDEX_RECENTS = 1;
56     public static final int TAB_INDEX_ALL_CONTACTS = 2;
57 
58     public static final int TAB_INDEX_COUNT = 3;
59 
60     private static final int MAX_RECENTS_ENTRIES = 20;
61     // Oldest recents entry to display is 2 weeks old.
62     private static final long OLDEST_RECENTS_DATE = 1000L * 60 * 60 * 24 * 14;
63 
64     private static final String KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE =
65             "key_last_dismissed_call_shortcut_date";
66 
67     public static final float REMOVE_VIEW_SHOWN_ALPHA = 0.5f;
68     public static final float REMOVE_VIEW_HIDDEN_ALPHA = 1;
69 
70     public interface HostInterface {
showCallHistory()71         public void showCallHistory();
getActionBarController()72         public ActionBarController getActionBarController();
73     }
74 
75     private ActionBar mActionBar;
76     private ViewPager mViewPager;
77     private ViewPagerTabs mViewPagerTabs;
78     private ViewPagerAdapter mViewPagerAdapter;
79     private ListView mShortcutCardsListView;
80     private RemoveView mRemoveView;
81     private View mRemoveViewContent;
82     private SpeedDialFragment mSpeedDialFragment;
83     private CallLogFragment mRecentsFragment;
84     private AllContactsFragment mAllContactsFragment;
85     private ArrayList<OnPageChangeListener> mOnPageChangeListeners =
86             new ArrayList<OnPageChangeListener>();
87 
88     private String[] mTabTitles;
89 
90     private ShortcutCardsAdapter mMergedAdapter;
91     private CallLogAdapter mCallLogAdapter;
92     private CallLogQueryHandler mCallLogQueryHandler;
93     private OverlappingPaneLayout mOverlappingPaneLayout;
94 
95     private boolean mIsPanelOpen = true;
96 
97     /**
98      * Call shortcuts older than this date (persisted in shared preferences) will not show up in
99      * at the top of the screen
100      */
101     private long mLastCallShortcutDate = 0;
102 
103     /**
104      * The date of the current call shortcut that is showing on screen.
105      */
106     private long mCurrentCallShortcutDate = 0;
107 
108     private PanelSlideCallbacks mPanelSlideCallbacks = new PanelSlideCallbacks() {
109         @Override
110         public void onPanelSlide(View panel, float slideOffset) {
111             // For every 1 percent that the panel is slid upwards, clip 1 percent off the top
112             // edge of the shortcut card, to achieve the animated effect of the shortcut card
113             // being pushed out of view when the panel is slid upwards. slideOffset is 1 when
114             // the shortcut card is fully exposed, and 0 when completely hidden.
115             float ratioCardHidden = (1 - slideOffset);
116             if (mShortcutCardsListView.getChildCount() > 0) {
117                 final SwipeableShortcutCard v =
118                         (SwipeableShortcutCard) mShortcutCardsListView.getChildAt(0);
119                 v.clipCard(ratioCardHidden);
120             }
121 
122             if (mActionBar != null) {
123                 // Amount of available space that is not being hidden by the bottom pane
124                 final int topPaneHeight = (int) (slideOffset * mShortcutCardsListView.getHeight());
125 
126                 final int availableActionBarHeight =
127                         Math.min(mActionBar.getHeight(), topPaneHeight);
128                 final ActionBarController controller =
129                         ((HostInterface) getActivity()).getActionBarController();
130                 controller.setHideOffset(mActionBar.getHeight() - availableActionBarHeight);
131 
132                 if (!mActionBar.isShowing()) {
133                     mActionBar.show();
134                 }
135             }
136         }
137 
138         @Override
139         public void onPanelOpened(View panel) {
140             if (DEBUG) {
141                 Log.d(TAG, "onPanelOpened");
142             }
143             mIsPanelOpen = true;
144         }
145 
146         @Override
147         public void onPanelClosed(View panel) {
148             if (DEBUG) {
149                 Log.d(TAG, "onPanelClosed");
150             }
151             mIsPanelOpen = false;
152         }
153 
154         @Override
155         public void onPanelFlingReachesEdge(int velocityY) {
156             if (getCurrentListView() != null) {
157                 getCurrentListView().fling(velocityY);
158             }
159         }
160 
161         @Override
162         public boolean isScrollableChildUnscrolled() {
163             final AbsListView listView = getCurrentListView();
164             return listView != null && (listView.getChildCount() == 0
165                     || listView.getChildAt(0).getTop() == listView.getPaddingTop());
166         }
167     };
168 
getCurrentListView()169     private AbsListView getCurrentListView() {
170         final int position = mViewPager.getCurrentItem();
171         switch (getRtlPosition(position)) {
172             case TAB_INDEX_SPEED_DIAL:
173                 return mSpeedDialFragment == null ? null : mSpeedDialFragment.getListView();
174             case TAB_INDEX_RECENTS:
175                 return mRecentsFragment == null ? null : mRecentsFragment.getListView();
176             case TAB_INDEX_ALL_CONTACTS:
177                 return mAllContactsFragment == null ? null : mAllContactsFragment.getListView();
178         }
179         throw new IllegalStateException("No fragment at position " + position);
180     }
181 
182     public class ViewPagerAdapter extends FragmentPagerAdapter {
ViewPagerAdapter(FragmentManager fm)183         public ViewPagerAdapter(FragmentManager fm) {
184             super(fm);
185         }
186 
187         @Override
getItemId(int position)188         public long getItemId(int position) {
189             return getRtlPosition(position);
190         }
191 
192         @Override
getItem(int position)193         public Fragment getItem(int position) {
194             switch (getRtlPosition(position)) {
195                 case TAB_INDEX_SPEED_DIAL:
196                     mSpeedDialFragment = new SpeedDialFragment();
197                     return mSpeedDialFragment;
198                 case TAB_INDEX_RECENTS:
199                     mRecentsFragment = new CallLogFragment(CallLogQueryHandler.CALL_TYPE_ALL,
200                             MAX_RECENTS_ENTRIES, System.currentTimeMillis() - OLDEST_RECENTS_DATE);
201                     mRecentsFragment.setHasFooterView(true);
202                     return mRecentsFragment;
203                 case TAB_INDEX_ALL_CONTACTS:
204                     mAllContactsFragment = new AllContactsFragment();
205                     return mAllContactsFragment;
206             }
207             throw new IllegalStateException("No fragment at position " + position);
208         }
209 
210         @Override
instantiateItem(ViewGroup container, int position)211         public Object instantiateItem(ViewGroup container, int position) {
212             // On rotation the FragmentManager handles rotation. Therefore getItem() isn't called.
213             // Copy the fragments that the FragmentManager finds so that we can store them in
214             // instance variables for later.
215             final Fragment fragment =
216                     (Fragment) super.instantiateItem(container, position);
217             if (fragment instanceof SpeedDialFragment) {
218                 mSpeedDialFragment = (SpeedDialFragment) fragment;
219             } else if (fragment instanceof CallLogFragment) {
220                 mRecentsFragment = (CallLogFragment) fragment;
221             } else if (fragment instanceof AllContactsFragment) {
222                 mAllContactsFragment = (AllContactsFragment) fragment;
223             }
224             return fragment;
225         }
226 
227         @Override
getCount()228         public int getCount() {
229             return TAB_INDEX_COUNT;
230         }
231 
232         @Override
getPageTitle(int position)233         public CharSequence getPageTitle(int position) {
234             return mTabTitles[position];
235         }
236     }
237 
238     @Override
onCreate(Bundle savedInstanceState)239     public void onCreate(Bundle savedInstanceState) {
240         super.onCreate(savedInstanceState);
241 
242         mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(),
243                 this, 1);
244         final String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
245         mCallLogAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this,
246                 new ContactInfoHelper(getActivity(), currentCountryIso), null, null, false);
247 
248         mMergedAdapter = new ShortcutCardsAdapter(getActivity(), this, mCallLogAdapter);
249     }
250 
251     @Override
onResume()252     public void onResume() {
253         super.onResume();
254         final SharedPreferences prefs = getActivity().getSharedPreferences(
255                 DialtactsActivity.SHARED_PREFS_NAME, Context.MODE_PRIVATE);
256         mLastCallShortcutDate = prefs.getLong(KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE, 0);
257         mActionBar = getActivity().getActionBar();
258         fetchCalls();
259         mCallLogAdapter.setLoading(true);
260         if (getUserVisibleHint()) {
261             sendScreenViewForPosition(mViewPager.getCurrentItem());
262         }
263     }
264 
265     @Override
onPause()266     public void onPause() {
267         // Wipe the cache to refresh the call shortcut item. This is not that expensive because
268         // it only contains one item.
269         mCallLogAdapter.invalidateCache();
270         super.onPause();
271     }
272 
273     @Override
onDestroy()274     public void onDestroy() {
275         mCallLogAdapter.stopRequestProcessing();
276         super.onDestroy();
277     }
278 
279     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)280     public View onCreateView(LayoutInflater inflater, ViewGroup container,
281             Bundle savedInstanceState) {
282         final View parentView = inflater.inflate(R.layout.lists_fragment, container, false);
283         mViewPager = (ViewPager) parentView.findViewById(R.id.lists_pager);
284         mViewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager());
285         mViewPager.setAdapter(mViewPagerAdapter);
286         mViewPager.setOffscreenPageLimit(2);
287         mViewPager.setOnPageChangeListener(this);
288         mViewPager.setCurrentItem(getRtlPosition(TAB_INDEX_SPEED_DIAL));
289 
290         mTabTitles = new String[TAB_INDEX_COUNT];
291         mTabTitles[TAB_INDEX_SPEED_DIAL] = getResources().getString(R.string.tab_speed_dial);
292         mTabTitles[TAB_INDEX_RECENTS] = getResources().getString(R.string.tab_recents);
293         mTabTitles[TAB_INDEX_ALL_CONTACTS] = getResources().getString(R.string.tab_all_contacts);
294 
295         mViewPagerTabs = (ViewPagerTabs) parentView.findViewById(R.id.lists_pager_header);
296         mViewPagerTabs.setViewPager(mViewPager);
297         addOnPageChangeListener(mViewPagerTabs);
298 
299         mShortcutCardsListView = (ListView) parentView.findViewById(R.id.shortcut_card_list);
300         mShortcutCardsListView.setAdapter(mMergedAdapter);
301 
302         mRemoveView = (RemoveView) parentView.findViewById(R.id.remove_view);
303         mRemoveViewContent = parentView.findViewById(R.id.remove_view_content);
304 
305         setupPaneLayout((OverlappingPaneLayout) parentView);
306         mOverlappingPaneLayout = (OverlappingPaneLayout) parentView;
307 
308         return parentView;
309     }
310 
311     @Override
onVoicemailStatusFetched(Cursor statusCursor)312     public void onVoicemailStatusFetched(Cursor statusCursor) {
313         // no-op
314     }
315 
316     @Override
onCallsFetched(Cursor cursor)317     public boolean onCallsFetched(Cursor cursor) {
318         mCallLogAdapter.setLoading(false);
319 
320         // Save the date of the most recent call log item
321         if (cursor != null && cursor.moveToFirst()) {
322             mCurrentCallShortcutDate = cursor.getLong(CallLogQuery.DATE);
323         }
324 
325         mCallLogAdapter.changeCursor(cursor);
326         mMergedAdapter.notifyDataSetChanged();
327 
328         // Refresh the overlapping pane to ensure that any changes in the shortcut card height
329         // are appropriately reflected in the overlap position.
330         mOverlappingPaneLayout.refresh();
331 
332         // Return true; took ownership of cursor
333         return true;
334     }
335 
336     @Override
fetchCalls()337     public void fetchCalls() {
338         mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL, mLastCallShortcutDate);
339     }
340 
dismissShortcut(View view)341     public void dismissShortcut(View view) {
342         mLastCallShortcutDate = mCurrentCallShortcutDate;
343         final SharedPreferences prefs = view.getContext().getSharedPreferences(
344                 DialtactsActivity.SHARED_PREFS_NAME, Context.MODE_PRIVATE);
345         prefs.edit().putLong(KEY_LAST_DISMISSED_CALL_SHORTCUT_DATE, mLastCallShortcutDate)
346                 .apply();
347         fetchCalls();
348     }
349 
addOnPageChangeListener(OnPageChangeListener onPageChangeListener)350     public void addOnPageChangeListener(OnPageChangeListener onPageChangeListener) {
351         if (!mOnPageChangeListeners.contains(onPageChangeListener)) {
352             mOnPageChangeListeners.add(onPageChangeListener);
353         }
354     }
355 
356     @Override
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)357     public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
358         final int count = mOnPageChangeListeners.size();
359         for (int i = 0; i < count; i++) {
360             mOnPageChangeListeners.get(i).onPageScrolled(position, positionOffset,
361                     positionOffsetPixels);
362         }
363     }
364 
365     @Override
onPageSelected(int position)366     public void onPageSelected(int position) {
367         final int count = mOnPageChangeListeners.size();
368         for (int i = 0; i < count; i++) {
369             mOnPageChangeListeners.get(i).onPageSelected(position);
370         }
371         sendScreenViewForPosition(position);
372     }
373 
374     @Override
onPageScrollStateChanged(int state)375     public void onPageScrollStateChanged(int state) {
376         final int count = mOnPageChangeListeners.size();
377         for (int i = 0; i < count; i++) {
378             mOnPageChangeListeners.get(i).onPageScrollStateChanged(state);
379         }
380     }
381 
showRemoveView(boolean show)382     public void showRemoveView(boolean show) {
383         mRemoveViewContent.setVisibility(show ? View.VISIBLE : View.GONE);
384         mRemoveView.setAlpha(show ? 0 : 1);
385         mRemoveView.animate().alpha(show ? 1 : 0).start();
386 
387         if (mShortcutCardsListView.getChildCount() > 0) {
388             View v = mShortcutCardsListView.getChildAt(0);
389             v.animate().withLayer()
390                     .alpha(show ? REMOVE_VIEW_SHOWN_ALPHA : REMOVE_VIEW_HIDDEN_ALPHA)
391                     .start();
392         }
393     }
394 
shouldShowActionBar()395     public boolean shouldShowActionBar() {
396         return mIsPanelOpen && mActionBar != null;
397     }
398 
isPaneOpen()399     public boolean isPaneOpen() {
400         return mIsPanelOpen;
401     }
402 
setupPaneLayout(OverlappingPaneLayout paneLayout)403     private void setupPaneLayout(OverlappingPaneLayout paneLayout) {
404         // TODO: Remove the notion of a capturable view. The entire view be slideable, once
405         // the framework better supports nested scrolling.
406         paneLayout.setCapturableView(mViewPagerTabs);
407         paneLayout.openPane();
408         paneLayout.setPanelSlideCallbacks(mPanelSlideCallbacks);
409         paneLayout.setIntermediatePinnedOffset(
410                 ((HostInterface) getActivity()).getActionBarController().getActionBarHeight());
411 
412         LayoutTransition transition = paneLayout.getLayoutTransition();
413         // Turns on animations for all types of layout changes so that they occur for
414         // height changes.
415         transition.enableTransitionType(LayoutTransition.CHANGING);
416     }
417 
getSpeedDialFragment()418     public SpeedDialFragment getSpeedDialFragment() {
419         return mSpeedDialFragment;
420     }
421 
getRemoveView()422     public RemoveView getRemoveView() {
423         return mRemoveView;
424     }
425 
getRtlPosition(int position)426     public int getRtlPosition(int position) {
427         if (DialerUtils.isRtl()) {
428             return TAB_INDEX_COUNT - 1 - position;
429         }
430         return position;
431     }
432 
sendScreenViewForCurrentPosition()433     public void sendScreenViewForCurrentPosition() {
434         sendScreenViewForPosition(mViewPager.getCurrentItem());
435     }
436 
sendScreenViewForPosition(int position)437     private void sendScreenViewForPosition(int position) {
438         if (!isResumed()) {
439             return;
440         }
441         String fragmentName;
442         switch (getRtlPosition(position)) {
443             case TAB_INDEX_SPEED_DIAL:
444                 fragmentName = SpeedDialFragment.class.getSimpleName();
445                 break;
446             case TAB_INDEX_RECENTS:
447                 fragmentName = CallLogFragment.class.getSimpleName();
448                 break;
449             case TAB_INDEX_ALL_CONTACTS:
450                 fragmentName = AllContactsFragment.class.getSimpleName();
451                 break;
452             default:
453                 return;
454         }
455         AnalyticsUtil.sendScreenView(fragmentName, getActivity(), null);
456     }
457 }
458