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