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