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 
17 package com.android.dialer.app.list;
18 
19 import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING;
20 
21 import android.app.Fragment;
22 import android.content.SharedPreferences;
23 import android.database.ContentObserver;
24 import android.database.Cursor;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.os.Trace;
28 import android.preference.PreferenceManager;
29 import android.provider.VoicemailContract;
30 import android.support.v4.view.ViewPager.OnPageChangeListener;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import com.android.contacts.common.list.ViewPagerTabs;
35 import com.android.dialer.app.R;
36 import com.android.dialer.app.calllog.CallLogFragment;
37 import com.android.dialer.app.calllog.CallLogFragment.CallLogFragmentListener;
38 import com.android.dialer.app.calllog.CallLogNotificationsService;
39 import com.android.dialer.app.calllog.VisualVoicemailCallLogFragment;
40 import com.android.dialer.common.LogUtil;
41 import com.android.dialer.database.CallLogQueryHandler;
42 import com.android.dialer.database.CallLogQueryHandler.Listener;
43 import com.android.dialer.logging.DialerImpression;
44 import com.android.dialer.logging.Logger;
45 import com.android.dialer.logging.ScreenEvent;
46 import com.android.dialer.logging.UiAction;
47 import com.android.dialer.performancereport.PerformanceReport;
48 import com.android.dialer.util.PermissionsUtil;
49 import com.android.dialer.voicemail.listui.error.VoicemailStatusCorruptionHandler;
50 import com.android.dialer.voicemail.listui.error.VoicemailStatusCorruptionHandler.Source;
51 import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker;
52 import com.android.dialer.voicemailstatus.VoicemailStatusHelper;
53 import java.util.ArrayList;
54 
55 /**
56  * Fragment that is used as the main screen of the Dialer.
57  *
58  * <p>Contains a ViewPager that contains various contact lists like the Speed Dial list and the All
59  * Contacts list. This will also eventually contain the logic that allows sliding the ViewPager
60  * containing the lists up above the search bar and pin it against the top of the screen.
61  */
62 public class ListsFragment extends Fragment
63     implements OnPageChangeListener, Listener, CallLogFragmentListener {
64 
65   private static final String TAG = "ListsFragment";
66 
67   private DialerViewPager viewPager;
68   private ViewPagerTabs viewPagerTabs;
69   private DialtactsPagerAdapter adapter;
70   private RemoveView removeView;
71   private View removeViewContent;
72   private Fragment currentPage;
73   private SharedPreferences prefs;
74   private boolean hasFetchedVoicemailStatus;
75   private boolean showVoicemailTabAfterVoicemailStatusIsFetched;
76   private final ArrayList<OnPageChangeListener> onPageChangeListeners = new ArrayList<>();
77   /** The position of the currently selected tab. */
78   private int tabIndex = DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL;
79 
80   private boolean paused;
81   private CallLogQueryHandler callLogQueryHandler;
82 
83   private UiAction.Type[] actionTypeList;
84   private final DialerImpression.Type[] swipeImpressionList =
85       new DialerImpression.Type[DialtactsPagerAdapter.TAB_COUNT_WITH_VOICEMAIL];
86   private final DialerImpression.Type[] clickImpressionList =
87       new DialerImpression.Type[DialtactsPagerAdapter.TAB_COUNT_WITH_VOICEMAIL];
88 
89   // Only for detecting page selected by swiping or clicking.
90   private boolean onPageScrolledBeforeScrollStateSettling;
91 
92   private final ContentObserver voicemailStatusObserver =
93       new ContentObserver(new Handler()) {
94         @Override
95         public void onChange(boolean selfChange) {
96           super.onChange(selfChange);
97           callLogQueryHandler.fetchVoicemailStatus();
98         }
99       };
100 
101   @Override
onCreate(Bundle savedInstanceState)102   public void onCreate(Bundle savedInstanceState) {
103     LogUtil.d("ListsFragment.onCreate", null);
104     Trace.beginSection(TAG + " onCreate");
105     super.onCreate(savedInstanceState);
106     prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
107     Trace.endSection();
108   }
109 
110   @Override
onResume()111   public void onResume() {
112     LogUtil.enterBlock("ListsFragment.onResume");
113     Trace.beginSection(TAG + " onResume");
114     super.onResume();
115 
116     paused = false;
117 
118     if (getUserVisibleHint()) {
119       sendScreenViewForCurrentPosition();
120     }
121 
122     // Fetch voicemail status to determine if we should show the voicemail tab.
123     callLogQueryHandler =
124         new CallLogQueryHandler(getActivity(), getActivity().getContentResolver(), this);
125     callLogQueryHandler.fetchVoicemailStatus();
126     callLogQueryHandler.fetchMissedCallsUnreadCount();
127     Trace.endSection();
128     currentPage = adapter.getItem(viewPager.getCurrentItem());
129   }
130 
131   @Override
onPause()132   public void onPause() {
133     LogUtil.enterBlock("ListsFragment.onPause");
134     super.onPause();
135 
136     paused = true;
137   }
138 
139   @Override
onDestroyView()140   public void onDestroyView() {
141     super.onDestroyView();
142     viewPager.removeOnPageChangeListener(this);
143   }
144 
145   @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)146   public View onCreateView(
147       LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
148     LogUtil.enterBlock("ListsFragment.onCreateView");
149     Trace.beginSection(TAG + " onCreateView");
150     Trace.beginSection(TAG + " inflate view");
151     final View parentView = inflater.inflate(R.layout.lists_fragment, container, false);
152     Trace.endSection();
153     Trace.beginSection(TAG + " setup views");
154 
155     actionTypeList = new UiAction.Type[DialtactsPagerAdapter.TAB_COUNT_WITH_VOICEMAIL];
156     actionTypeList[DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL] =
157         UiAction.Type.CHANGE_TAB_TO_FAVORITE;
158     actionTypeList[DialtactsPagerAdapter.TAB_INDEX_HISTORY] = UiAction.Type.CHANGE_TAB_TO_CALL_LOG;
159     actionTypeList[DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS] =
160         UiAction.Type.CHANGE_TAB_TO_CONTACTS;
161     actionTypeList[DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL] =
162         UiAction.Type.CHANGE_TAB_TO_VOICEMAIL;
163 
164     swipeImpressionList[DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL] =
165         DialerImpression.Type.SWITCH_TAB_TO_FAVORITE_BY_SWIPE;
166     swipeImpressionList[DialtactsPagerAdapter.TAB_INDEX_HISTORY] =
167         DialerImpression.Type.SWITCH_TAB_TO_CALL_LOG_BY_SWIPE;
168     swipeImpressionList[DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS] =
169         DialerImpression.Type.SWITCH_TAB_TO_CONTACTS_BY_SWIPE;
170     swipeImpressionList[DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL] =
171         DialerImpression.Type.SWITCH_TAB_TO_VOICEMAIL_BY_SWIPE;
172 
173     clickImpressionList[DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL] =
174         DialerImpression.Type.SWITCH_TAB_TO_FAVORITE_BY_CLICK;
175     clickImpressionList[DialtactsPagerAdapter.TAB_INDEX_HISTORY] =
176         DialerImpression.Type.SWITCH_TAB_TO_CALL_LOG_BY_CLICK;
177     clickImpressionList[DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS] =
178         DialerImpression.Type.SWITCH_TAB_TO_CONTACTS_BY_CLICK;
179     clickImpressionList[DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL] =
180         DialerImpression.Type.SWITCH_TAB_TO_VOICEMAIL_BY_CLICK;
181 
182     String[] tabTitles = new String[DialtactsPagerAdapter.TAB_COUNT_WITH_VOICEMAIL];
183     tabTitles[DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL] =
184         getResources().getString(R.string.tab_speed_dial);
185     tabTitles[DialtactsPagerAdapter.TAB_INDEX_HISTORY] =
186         getResources().getString(R.string.tab_history);
187     tabTitles[DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS] =
188         getResources().getString(R.string.tab_all_contacts);
189     tabTitles[DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL] =
190         getResources().getString(R.string.tab_voicemail);
191 
192     int[] tabIcons = new int[DialtactsPagerAdapter.TAB_COUNT_WITH_VOICEMAIL];
193     tabIcons[DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL] = R.drawable.quantum_ic_grade_white_24;
194     tabIcons[DialtactsPagerAdapter.TAB_INDEX_HISTORY] = R.drawable.quantum_ic_schedule_white_24;
195     tabIcons[DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS] = R.drawable.quantum_ic_people_white_24;
196     tabIcons[DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL] = R.drawable.quantum_ic_voicemail_white_24;
197 
198     viewPager = (DialerViewPager) parentView.findViewById(R.id.lists_pager);
199     adapter =
200         new DialtactsPagerAdapter(
201             getChildFragmentManager(),
202             tabTitles,
203             prefs.getBoolean(
204                 VisualVoicemailEnabledChecker.PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, false));
205     viewPager.setAdapter(adapter);
206 
207     // This is deliberate. See cl/172018946 for the app startup implications of using 1 here
208     // versus loading more fragments upfront.
209     viewPager.setOffscreenPageLimit(1);
210 
211     viewPager.addOnPageChangeListener(this);
212     showTab(DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL);
213 
214     viewPagerTabs = (ViewPagerTabs) parentView.findViewById(R.id.lists_pager_header);
215     viewPagerTabs.configureTabIcons(tabIcons);
216     viewPagerTabs.setViewPager(viewPager);
217     addOnPageChangeListener(viewPagerTabs);
218     removeView = (RemoveView) parentView.findViewById(R.id.remove_view);
219     removeViewContent = parentView.findViewById(R.id.remove_view_content);
220 
221     if (PermissionsUtil.hasReadVoicemailPermissions(getContext())
222         && PermissionsUtil.hasAddVoicemailPermissions(getContext())) {
223       getActivity()
224           .getContentResolver()
225           .registerContentObserver(
226               VoicemailContract.Status.CONTENT_URI, true, voicemailStatusObserver);
227     } else {
228       LogUtil.w("ListsFragment.onCreateView", "no voicemail read permissions");
229     }
230 
231     Trace.endSection();
232     Trace.endSection();
233     return parentView;
234   }
235 
236   @Override
onDestroy()237   public void onDestroy() {
238     getActivity().getContentResolver().unregisterContentObserver(voicemailStatusObserver);
239     super.onDestroy();
240   }
241 
addOnPageChangeListener(OnPageChangeListener onPageChangeListener)242   public void addOnPageChangeListener(OnPageChangeListener onPageChangeListener) {
243     if (!onPageChangeListeners.contains(onPageChangeListener)) {
244       onPageChangeListeners.add(onPageChangeListener);
245     }
246   }
247 
248   /**
249    * Shows the tab with the specified index. If the voicemail tab index is specified, but the
250    * voicemail status hasn't been fetched, it will show the speed dial tab and try to show the
251    * voicemail tab after the voicemail status has been fetched.
252    */
showTab(int index)253   public void showTab(int index) {
254     if (index == DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL) {
255       if (adapter.hasActiveVoicemailProvider()) {
256         viewPager.setCurrentItem(adapter.getRtlPosition(DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL));
257       } else if (!hasFetchedVoicemailStatus) {
258         // Try to show the voicemail tab after the voicemail status returns.
259         showVoicemailTabAfterVoicemailStatusIsFetched = true;
260       }
261     } else if (index < getTabCount()) {
262       viewPager.setCurrentItem(adapter.getRtlPosition(index));
263     }
264   }
265 
266   @Override
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)267   public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
268     // onPageScrolled(0, 0, 0) is called when app launch. And we should ignore it.
269     // It's also called when swipe right from first tab, but we don't care.
270     if (positionOffsetPixels != 0) {
271       onPageScrolledBeforeScrollStateSettling = true;
272     }
273     tabIndex = adapter.getRtlPosition(position);
274 
275     final int count = onPageChangeListeners.size();
276     for (int i = 0; i < count; i++) {
277       onPageChangeListeners.get(i).onPageScrolled(position, positionOffset, positionOffsetPixels);
278     }
279   }
280 
281   @Override
onPageSelected(int position)282   public void onPageSelected(int position) {
283     // onPageScrollStateChanged(SCROLL_STATE_SETTLING) must be called before this.
284     // If onPageScrolled() is called before that, the page is selected by swiping;
285     // otherwise the page is selected by clicking.
286     if (onPageScrolledBeforeScrollStateSettling) {
287       Logger.get(getContext()).logImpression(swipeImpressionList[position]);
288       onPageScrolledBeforeScrollStateSettling = false;
289     } else {
290       Logger.get(getContext()).logImpression(clickImpressionList[position]);
291     }
292 
293     PerformanceReport.recordClick(actionTypeList[position]);
294 
295     LogUtil.i("ListsFragment.onPageSelected", "position: %d", position);
296     tabIndex = adapter.getRtlPosition(position);
297 
298     // Show the tab which has been selected instead.
299     showVoicemailTabAfterVoicemailStatusIsFetched = false;
300 
301     final int count = onPageChangeListeners.size();
302     for (int i = 0; i < count; i++) {
303       onPageChangeListeners.get(i).onPageSelected(position);
304     }
305     sendScreenViewForCurrentPosition();
306 
307     if (currentPage instanceof CallLogFragment) {
308       ((CallLogFragment) currentPage).onNotVisible();
309     }
310     currentPage = adapter.getItem(position);
311     if (currentPage instanceof CallLogFragment) {
312       ((CallLogFragment) currentPage).onVisible();
313     }
314   }
315 
316   @Override
onPageScrollStateChanged(int state)317   public void onPageScrollStateChanged(int state) {
318     if (state != SCROLL_STATE_SETTLING) {
319       onPageScrolledBeforeScrollStateSettling = false;
320     }
321 
322     final int count = onPageChangeListeners.size();
323     for (int i = 0; i < count; i++) {
324       onPageChangeListeners.get(i).onPageScrollStateChanged(state);
325     }
326   }
327 
328   @Override
onVoicemailStatusFetched(Cursor statusCursor)329   public void onVoicemailStatusFetched(Cursor statusCursor) {
330     hasFetchedVoicemailStatus = true;
331 
332     if (getActivity() == null || paused) {
333       return;
334     }
335 
336     VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus(
337         getContext(), statusCursor, Source.Activity);
338 
339     // Update hasActiveVoicemailProvider, which controls the number of tabs displayed.
340     boolean hasActiveVoicemailProvider =
341         VoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor) > 0;
342     if (hasActiveVoicemailProvider != adapter.hasActiveVoicemailProvider()) {
343       adapter.setHasActiveVoicemailProvider(hasActiveVoicemailProvider);
344       adapter.notifyDataSetChanged();
345 
346       if (hasActiveVoicemailProvider) {
347         Logger.get(getContext()).logImpression(DialerImpression.Type.VVM_TAB_VISIBLE);
348         viewPagerTabs.updateTab(DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL);
349       } else {
350         viewPagerTabs.removeTab(DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL);
351         adapter.removeVoicemailFragment(getChildFragmentManager());
352       }
353 
354       prefs
355           .edit()
356           .putBoolean(
357               VisualVoicemailEnabledChecker.PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER,
358               hasActiveVoicemailProvider)
359           .apply();
360     }
361 
362     if (hasActiveVoicemailProvider) {
363       callLogQueryHandler.fetchVoicemailUnreadCount();
364     }
365 
366     if (adapter.hasActiveVoicemailProvider() && showVoicemailTabAfterVoicemailStatusIsFetched) {
367       showVoicemailTabAfterVoicemailStatusIsFetched = false;
368       showTab(DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL);
369     }
370   }
371 
372   @Override
onVoicemailUnreadCountFetched(Cursor cursor)373   public void onVoicemailUnreadCountFetched(Cursor cursor) {
374     if (getActivity() == null || getActivity().isFinishing() || cursor == null) {
375       return;
376     }
377 
378     int count = 0;
379     try {
380       count = cursor.getCount();
381     } finally {
382       cursor.close();
383     }
384 
385     viewPagerTabs.setUnreadCount(count, DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL);
386     viewPagerTabs.updateTab(DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL);
387   }
388 
389   @Override
onMissedCallsUnreadCountFetched(Cursor cursor)390   public void onMissedCallsUnreadCountFetched(Cursor cursor) {
391     if (getActivity() == null || getActivity().isFinishing() || cursor == null) {
392       return;
393     }
394 
395     int count = 0;
396     try {
397       count = cursor.getCount();
398     } finally {
399       cursor.close();
400     }
401 
402     viewPagerTabs.setUnreadCount(count, DialtactsPagerAdapter.TAB_INDEX_HISTORY);
403     viewPagerTabs.updateTab(DialtactsPagerAdapter.TAB_INDEX_HISTORY);
404   }
405 
406   @Override
onCallsFetched(Cursor statusCursor)407   public boolean onCallsFetched(Cursor statusCursor) {
408     // Return false; did not take ownership of cursor
409     return false;
410   }
411 
getCurrentTabIndex()412   public int getCurrentTabIndex() {
413     return tabIndex;
414   }
415 
shouldShowFab()416   public boolean shouldShowFab() {
417     // If the VVM TOS is visible, don't show the fab
418     if (currentPage instanceof VisualVoicemailCallLogFragment
419         && ((VisualVoicemailCallLogFragment) currentPage).isModalAlertVisible()) {
420       return false;
421     }
422 
423     return true;
424   }
425 
426   @Override
updateTabUnreadCounts()427   public void updateTabUnreadCounts() {
428     if (callLogQueryHandler != null) {
429       callLogQueryHandler.fetchMissedCallsUnreadCount();
430       if (adapter.hasActiveVoicemailProvider()) {
431         callLogQueryHandler.fetchVoicemailUnreadCount();
432       }
433     }
434   }
435 
436   /** External method to mark all missed calls as read. */
markMissedCallsAsReadAndRemoveNotifications()437   public void markMissedCallsAsReadAndRemoveNotifications() {
438     if (callLogQueryHandler != null) {
439       callLogQueryHandler.markMissedCallsAsRead();
440       CallLogNotificationsService.cancelAllMissedCalls(getContext());
441     }
442   }
443 
showRemoveView(boolean show)444   public void showRemoveView(boolean show) {
445     removeViewContent.setVisibility(show ? View.VISIBLE : View.GONE);
446     removeView.setAlpha(show ? 0 : 1);
447     removeView.animate().alpha(show ? 1 : 0).start();
448   }
449 
450   @Override
showMultiSelectRemoveView(boolean show)451   public void showMultiSelectRemoveView(boolean show) {
452     viewPagerTabs.setVisibility(show ? View.GONE : View.VISIBLE);
453     viewPager.setEnableSwipingPages(!show);
454   }
455 
hasFrequents()456   public boolean hasFrequents() {
457     OldSpeedDialFragment page =
458         (OldSpeedDialFragment)
459             adapter.getItem(adapter.getRtlPosition(DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL));
460     return page.hasFrequents();
461   }
462 
getRemoveView()463   public RemoveView getRemoveView() {
464     return removeView;
465   }
466 
getTabCount()467   public int getTabCount() {
468     return adapter.getCount();
469   }
470 
sendScreenViewForCurrentPosition()471   public void sendScreenViewForCurrentPosition() {
472     if (!isResumed()) {
473       return;
474     }
475 
476     ScreenEvent.Type screenType;
477     switch (getCurrentTabIndex()) {
478       case DialtactsPagerAdapter.TAB_INDEX_SPEED_DIAL:
479         screenType = ScreenEvent.Type.SPEED_DIAL;
480         break;
481       case DialtactsPagerAdapter.TAB_INDEX_HISTORY:
482         screenType = ScreenEvent.Type.CALL_LOG;
483         break;
484       case DialtactsPagerAdapter.TAB_INDEX_ALL_CONTACTS:
485         screenType = ScreenEvent.Type.ALL_CONTACTS;
486         break;
487       case DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL:
488         screenType = ScreenEvent.Type.VOICEMAIL_LOG;
489         break;
490       default:
491         return;
492     }
493     Logger.get(getActivity()).logScreenView(screenType, getActivity());
494   }
495 }
496