1 /* 2 * Copyright (C) 2010 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.contacts.activities; 18 19 import android.app.Fragment; 20 import android.app.FragmentManager; 21 import android.app.FragmentTransaction; 22 import android.content.Intent; 23 import android.graphics.Rect; 24 import android.net.Uri; 25 import android.os.Bundle; 26 import android.os.Parcelable; 27 import android.os.UserManager; 28 import android.preference.PreferenceActivity; 29 import android.provider.ContactsContract; 30 import android.provider.ContactsContract.Contacts; 31 import android.provider.ContactsContract.ProviderStatus; 32 import android.provider.ContactsContract.QuickContact; 33 import android.provider.Settings; 34 import android.support.v13.app.FragmentPagerAdapter; 35 import android.support.v4.view.PagerAdapter; 36 import android.support.v4.view.ViewPager; 37 import android.text.TextUtils; 38 import android.util.Log; 39 import android.view.KeyCharacterMap; 40 import android.view.KeyEvent; 41 import android.view.Menu; 42 import android.view.MenuInflater; 43 import android.view.MenuItem; 44 import android.view.View; 45 import android.view.ViewGroup; 46 import android.view.Window; 47 import android.widget.ImageButton; 48 import android.widget.Toolbar; 49 50 import com.android.contacts.ContactsActivity; 51 import com.android.contacts.R; 52 import com.android.contacts.activities.ActionBarAdapter.TabState; 53 import com.android.contacts.common.ContactsUtils; 54 import com.android.contacts.common.dialog.ClearFrequentsDialog; 55 import com.android.contacts.interactions.ContactDeletionInteraction; 56 import com.android.contacts.common.interactions.ImportExportDialogFragment; 57 import com.android.contacts.common.list.ContactEntryListFragment; 58 import com.android.contacts.common.list.ContactListFilter; 59 import com.android.contacts.common.list.ContactListFilterController; 60 import com.android.contacts.common.list.ContactTileAdapter.DisplayType; 61 import com.android.contacts.list.ContactTileListFragment; 62 import com.android.contacts.list.ContactsIntentResolver; 63 import com.android.contacts.list.ContactsRequest; 64 import com.android.contacts.list.ContactsUnavailableFragment; 65 import com.android.contacts.list.DefaultContactBrowseListFragment; 66 import com.android.contacts.common.list.DirectoryListLoader; 67 import com.android.contacts.common.preference.DisplayOptionsPreferenceFragment; 68 import com.android.contacts.list.OnContactBrowserActionListener; 69 import com.android.contacts.list.OnContactsUnavailableActionListener; 70 import com.android.contacts.list.ProviderStatusWatcher; 71 import com.android.contacts.list.ProviderStatusWatcher.ProviderStatusListener; 72 import com.android.contacts.common.list.ViewPagerTabs; 73 import com.android.contacts.preference.ContactsPreferenceActivity; 74 import com.android.contacts.common.util.AccountFilterUtil; 75 import com.android.contacts.common.util.ViewUtil; 76 import com.android.contacts.quickcontact.QuickContactActivity; 77 import com.android.contacts.util.AccountPromptUtils; 78 import com.android.contacts.common.util.Constants; 79 import com.android.contacts.util.DialogManager; 80 import com.android.contacts.util.HelpUtils; 81 82 import java.util.Locale; 83 import java.util.concurrent.atomic.AtomicInteger; 84 85 /** 86 * Displays a list to browse contacts. 87 */ 88 public class PeopleActivity extends ContactsActivity implements 89 View.OnCreateContextMenuListener, 90 View.OnClickListener, 91 ActionBarAdapter.Listener, 92 DialogManager.DialogShowingViewActivity, 93 ContactListFilterController.ContactListFilterListener, 94 ProviderStatusListener { 95 96 private static final String TAG = "PeopleActivity"; 97 98 private static final String ENABLE_DEBUG_OPTIONS_HIDDEN_CODE = "debug debug!"; 99 100 // These values needs to start at 2. See {@link ContactEntryListFragment}. 101 private static final int SUBACTIVITY_ACCOUNT_FILTER = 2; 102 103 private final DialogManager mDialogManager = new DialogManager(this); 104 105 private ContactsIntentResolver mIntentResolver; 106 private ContactsRequest mRequest; 107 108 private ActionBarAdapter mActionBarAdapter; 109 110 private ContactTileListFragment.Listener mFavoritesFragmentListener = 111 new StrequentContactListFragmentListener(); 112 113 private ContactListFilterController mContactListFilterController; 114 115 private ContactsUnavailableFragment mContactsUnavailableFragment; 116 private ProviderStatusWatcher mProviderStatusWatcher; 117 private ProviderStatusWatcher.Status mProviderStatus; 118 119 private boolean mOptionsMenuContactsAvailable; 120 121 /** 122 * Showing a list of Contacts. Also used for showing search results in search mode. 123 */ 124 private DefaultContactBrowseListFragment mAllFragment; 125 private ContactTileListFragment mFavoritesFragment; 126 127 /** ViewPager for swipe */ 128 private ViewPager mTabPager; 129 private ViewPagerTabs mViewPagerTabs; 130 private TabPagerAdapter mTabPagerAdapter; 131 private String[] mTabTitles; 132 private final TabPagerListener mTabPagerListener = new TabPagerListener(); 133 134 private boolean mEnableDebugMenuOptions; 135 136 /** 137 * True if this activity instance is a re-created one. i.e. set true after orientation change. 138 * This is set in {@link #onCreate} for later use in {@link #onStart}. 139 */ 140 private boolean mIsRecreatedInstance; 141 142 /** 143 * If {@link #configureFragments(boolean)} is already called. Used to avoid calling it twice 144 * in {@link #onStart}. 145 * (This initialization only needs to be done once in onStart() when the Activity was just 146 * created from scratch -- i.e. onCreate() was just called) 147 */ 148 private boolean mFragmentInitialized; 149 150 /** 151 * This is to disable {@link #onOptionsItemSelected} when we trying to stop the activity. 152 */ 153 private boolean mDisableOptionItemSelected; 154 155 /** Sequential ID assigned to each instance; used for logging */ 156 private final int mInstanceId; 157 private static final AtomicInteger sNextInstanceId = new AtomicInteger(); 158 159 public PeopleActivity() { 160 mInstanceId = sNextInstanceId.getAndIncrement(); 161 mIntentResolver = new ContactsIntentResolver(this); 162 mProviderStatusWatcher = ProviderStatusWatcher.getInstance(this); 163 } 164 165 @Override 166 public String toString() { 167 // Shown on logcat 168 return String.format("%s@%d", getClass().getSimpleName(), mInstanceId); 169 } 170 171 public boolean areContactsAvailable() { 172 return (mProviderStatus != null) 173 && mProviderStatus.status == ProviderStatus.STATUS_NORMAL; 174 } 175 176 private boolean areContactWritableAccountsAvailable() { 177 return ContactsUtils.areContactWritableAccountsAvailable(this); 178 } 179 180 private boolean areGroupWritableAccountsAvailable() { 181 return ContactsUtils.areGroupWritableAccountsAvailable(this); 182 } 183 184 /** 185 * Initialize fragments that are (or may not be) in the layout. 186 * 187 * For the fragments that are in the layout, we initialize them in 188 * {@link #createViewsAndFragments(Bundle)} after inflating the layout. 189 * 190 * However, the {@link ContactsUnavailableFragment} is a special fragment which may not 191 * be in the layout, so we have to do the initialization here. 192 * 193 * The ContactsUnavailableFragment is always created at runtime. 194 */ 195 @Override 196 public void onAttachFragment(Fragment fragment) { 197 if (fragment instanceof ContactsUnavailableFragment) { 198 mContactsUnavailableFragment = (ContactsUnavailableFragment)fragment; 199 mContactsUnavailableFragment.setOnContactsUnavailableActionListener( 200 new ContactsUnavailableFragmentListener()); 201 } 202 } 203 204 @Override 205 protected void onCreate(Bundle savedState) { 206 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 207 Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate start"); 208 } 209 super.onCreate(savedState); 210 211 if (!processIntent(false)) { 212 finish(); 213 return; 214 } 215 mContactListFilterController = ContactListFilterController.getInstance(this); 216 mContactListFilterController.checkFilterValidity(false); 217 mContactListFilterController.addListener(this); 218 219 mProviderStatusWatcher.addListener(this); 220 221 mIsRecreatedInstance = (savedState != null); 222 createViewsAndFragments(savedState); 223 224 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 225 Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate finish"); 226 } 227 getWindow().setBackgroundDrawable(null); 228 } 229 230 @Override 231 protected void onNewIntent(Intent intent) { 232 setIntent(intent); 233 if (!processIntent(true)) { 234 finish(); 235 return; 236 } 237 mActionBarAdapter.initialize(null, mRequest); 238 239 mContactListFilterController.checkFilterValidity(false); 240 241 // Re-configure fragments. 242 configureFragments(true /* from request */); 243 invalidateOptionsMenuIfNeeded(); 244 } 245 246 /** 247 * Resolve the intent and initialize {@link #mRequest}, and launch another activity if redirect 248 * is needed. 249 * 250 * @param forNewIntent set true if it's called from {@link #onNewIntent(Intent)}. 251 * @return {@code true} if {@link PeopleActivity} should continue running. {@code false} 252 * if it shouldn't, in which case the caller should finish() itself and shouldn't do 253 * farther initialization. 254 */ 255 private boolean processIntent(boolean forNewIntent) { 256 // Extract relevant information from the intent 257 mRequest = mIntentResolver.resolveIntent(getIntent()); 258 if (Log.isLoggable(TAG, Log.DEBUG)) { 259 Log.d(TAG, this + " processIntent: forNewIntent=" + forNewIntent 260 + " intent=" + getIntent() + " request=" + mRequest); 261 } 262 if (!mRequest.isValid()) { 263 setResult(RESULT_CANCELED); 264 return false; 265 } 266 267 Intent redirect = mRequest.getRedirectIntent(); 268 if (redirect != null) { 269 // Need to start a different activity 270 startActivity(redirect); 271 return false; 272 } 273 274 if (mRequest.getActionCode() == ContactsRequest.ACTION_VIEW_CONTACT) { 275 redirect = new Intent(this, QuickContactActivity.class); 276 redirect.setAction(Intent.ACTION_VIEW); 277 redirect.setData(mRequest.getContactUri()); 278 startActivity(redirect); 279 return false; 280 } 281 return true; 282 } 283 284 private void createViewsAndFragments(Bundle savedState) { 285 // Disable the ActionBar so that we can use a Toolbar. This needs to be called before 286 // setContentView(). 287 getWindow().requestFeature(Window.FEATURE_NO_TITLE); 288 289 setContentView(R.layout.people_activity); 290 291 final FragmentManager fragmentManager = getFragmentManager(); 292 293 // Hide all tabs (the current tab will later be reshown once a tab is selected) 294 final FragmentTransaction transaction = fragmentManager.beginTransaction(); 295 296 mTabTitles = new String[TabState.COUNT]; 297 mTabTitles[TabState.FAVORITES] = getString(R.string.favorites_tab_label); 298 mTabTitles[TabState.ALL] = getString(R.string.all_contacts_tab_label); 299 mTabPager = getView(R.id.tab_pager); 300 mTabPagerAdapter = new TabPagerAdapter(); 301 mTabPager.setAdapter(mTabPagerAdapter); 302 mTabPager.setOnPageChangeListener(mTabPagerListener); 303 304 // Configure toolbar and toolbar tabs. If in landscape mode, we configure tabs differntly. 305 final Toolbar toolbar = getView(R.id.toolbar); 306 setActionBar(toolbar); 307 final ViewPagerTabs portraitViewPagerTabs 308 = (ViewPagerTabs) findViewById(R.id.lists_pager_header); 309 ViewPagerTabs landscapeViewPagerTabs = null; 310 if (portraitViewPagerTabs == null) { 311 landscapeViewPagerTabs = (ViewPagerTabs) getLayoutInflater().inflate( 312 R.layout.people_activity_tabs_lands, toolbar, /* attachToRoot = */ false); 313 mViewPagerTabs = landscapeViewPagerTabs; 314 } else { 315 mViewPagerTabs = portraitViewPagerTabs; 316 } 317 mViewPagerTabs.setViewPager(mTabPager); 318 319 final String FAVORITE_TAG = "tab-pager-favorite"; 320 final String ALL_TAG = "tab-pager-all"; 321 322 // Create the fragments and add as children of the view pager. 323 // The pager adapter will only change the visibility; it'll never create/destroy 324 // fragments. 325 // However, if it's after screen rotation, the fragments have been re-created by 326 // the fragment manager, so first see if there're already the target fragments 327 // existing. 328 mFavoritesFragment = (ContactTileListFragment) 329 fragmentManager.findFragmentByTag(FAVORITE_TAG); 330 mAllFragment = (DefaultContactBrowseListFragment) 331 fragmentManager.findFragmentByTag(ALL_TAG); 332 333 if (mFavoritesFragment == null) { 334 mFavoritesFragment = new ContactTileListFragment(); 335 mAllFragment = new DefaultContactBrowseListFragment(); 336 337 transaction.add(R.id.tab_pager, mFavoritesFragment, FAVORITE_TAG); 338 transaction.add(R.id.tab_pager, mAllFragment, ALL_TAG); 339 } 340 341 mFavoritesFragment.setListener(mFavoritesFragmentListener); 342 343 mAllFragment.setOnContactListActionListener(new ContactBrowserActionListener()); 344 345 // Hide all fragments for now. We adjust visibility when we get onSelectedTabChanged() 346 // from ActionBarAdapter. 347 transaction.hide(mFavoritesFragment); 348 transaction.hide(mAllFragment); 349 350 transaction.commitAllowingStateLoss(); 351 fragmentManager.executePendingTransactions(); 352 353 // Setting Properties after fragment is created 354 mFavoritesFragment.setDisplayType(DisplayType.STREQUENT); 355 356 mActionBarAdapter = new ActionBarAdapter(this, this, getActionBar(), 357 portraitViewPagerTabs, landscapeViewPagerTabs, toolbar); 358 mActionBarAdapter.initialize(savedState, mRequest); 359 360 // Add shadow under toolbar 361 ViewUtil.addRectangularOutlineProvider(findViewById(R.id.toolbar_parent), getResources()); 362 363 // Configure action button 364 final View floatingActionButtonContainer = findViewById( 365 R.id.floating_action_button_container); 366 ViewUtil.setupFloatingActionButton(floatingActionButtonContainer, getResources()); 367 final ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button); 368 floatingActionButton.setOnClickListener(this); 369 370 invalidateOptionsMenuIfNeeded(); 371 } 372 373 @Override 374 protected void onStart() { 375 if (!mFragmentInitialized) { 376 mFragmentInitialized = true; 377 /* Configure fragments if we haven't. 378 * 379 * Note it's a one-shot initialization, so we want to do this in {@link #onCreate}. 380 * 381 * However, because this method may indirectly touch views in fragments but fragments 382 * created in {@link #configureContentView} using a {@link FragmentTransaction} will NOT 383 * have views until {@link Activity#onCreate} finishes (they would if they were inflated 384 * from a layout), we need to do it here in {@link #onStart()}. 385 * 386 * (When {@link Fragment#onCreateView} is called is different in the former case and 387 * in the latter case, unfortunately.) 388 * 389 * Also, we skip most of the work in it if the activity is a re-created one. 390 * (so the argument.) 391 */ 392 configureFragments(!mIsRecreatedInstance); 393 } 394 super.onStart(); 395 } 396 397 @Override 398 protected void onPause() { 399 mOptionsMenuContactsAvailable = false; 400 mProviderStatusWatcher.stop(); 401 super.onPause(); 402 } 403 404 @Override 405 protected void onResume() { 406 super.onResume(); 407 408 mProviderStatusWatcher.start(); 409 updateViewConfiguration(true); 410 411 // Re-register the listener, which may have been cleared when onSaveInstanceState was 412 // called. See also: onSaveInstanceState 413 mActionBarAdapter.setListener(this); 414 mDisableOptionItemSelected = false; 415 if (mTabPager != null) { 416 mTabPager.setOnPageChangeListener(mTabPagerListener); 417 } 418 // Current tab may have changed since the last onSaveInstanceState(). Make sure 419 // the actual contents match the tab. 420 updateFragmentsVisibility(); 421 } 422 423 @Override 424 protected void onStop() { 425 super.onStop(); 426 } 427 428 @Override 429 protected void onDestroy() { 430 mProviderStatusWatcher.removeListener(this); 431 432 // Some of variables will be null if this Activity redirects Intent. 433 // See also onCreate() or other methods called during the Activity's initialization. 434 if (mActionBarAdapter != null) { 435 mActionBarAdapter.setListener(null); 436 } 437 if (mContactListFilterController != null) { 438 mContactListFilterController.removeListener(this); 439 } 440 441 super.onDestroy(); 442 } 443 444 private void configureFragments(boolean fromRequest) { 445 if (fromRequest) { 446 ContactListFilter filter = null; 447 int actionCode = mRequest.getActionCode(); 448 boolean searchMode = mRequest.isSearchMode(); 449 final int tabToOpen; 450 switch (actionCode) { 451 case ContactsRequest.ACTION_ALL_CONTACTS: 452 filter = ContactListFilter.createFilterWithType( 453 ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS); 454 tabToOpen = TabState.ALL; 455 break; 456 case ContactsRequest.ACTION_CONTACTS_WITH_PHONES: 457 filter = ContactListFilter.createFilterWithType( 458 ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY); 459 tabToOpen = TabState.ALL; 460 break; 461 462 case ContactsRequest.ACTION_FREQUENT: 463 case ContactsRequest.ACTION_STREQUENT: 464 case ContactsRequest.ACTION_STARRED: 465 tabToOpen = TabState.FAVORITES; 466 break; 467 case ContactsRequest.ACTION_VIEW_CONTACT: 468 tabToOpen = TabState.ALL; 469 break; 470 default: 471 tabToOpen = -1; 472 break; 473 } 474 if (tabToOpen != -1) { 475 mActionBarAdapter.setCurrentTab(tabToOpen); 476 } 477 478 if (filter != null) { 479 mContactListFilterController.setContactListFilter(filter, false); 480 searchMode = false; 481 } 482 483 if (mRequest.getContactUri() != null) { 484 searchMode = false; 485 } 486 487 mActionBarAdapter.setSearchMode(searchMode); 488 configureContactListFragmentForRequest(); 489 } 490 491 configureContactListFragment(); 492 493 invalidateOptionsMenuIfNeeded(); 494 } 495 496 @Override 497 public void onContactListFilterChanged() { 498 if (mAllFragment == null || !mAllFragment.isAdded()) { 499 return; 500 } 501 502 mAllFragment.setFilter(mContactListFilterController.getFilter()); 503 504 invalidateOptionsMenuIfNeeded(); 505 } 506 507 /** 508 * Handler for action bar actions. 509 */ 510 @Override 511 public void onAction(int action) { 512 switch (action) { 513 case ActionBarAdapter.Listener.Action.START_SEARCH_MODE: 514 // Tell the fragments that we're in the search mode 515 configureFragments(false /* from request */); 516 updateFragmentsVisibility(); 517 invalidateOptionsMenu(); 518 break; 519 case ActionBarAdapter.Listener.Action.STOP_SEARCH_MODE: 520 setQueryTextToFragment(""); 521 updateFragmentsVisibility(); 522 invalidateOptionsMenu(); 523 break; 524 case ActionBarAdapter.Listener.Action.CHANGE_SEARCH_QUERY: 525 final String queryString = mActionBarAdapter.getQueryString(); 526 setQueryTextToFragment(queryString); 527 updateDebugOptionsVisibility( 528 ENABLE_DEBUG_OPTIONS_HIDDEN_CODE.equals(queryString)); 529 break; 530 default: 531 throw new IllegalStateException("Unkonwn ActionBarAdapter action: " + action); 532 } 533 } 534 535 @Override 536 public void onSelectedTabChanged() { 537 updateFragmentsVisibility(); 538 } 539 540 @Override 541 public void onUpButtonPressed() { 542 onBackPressed(); 543 } 544 545 private void updateDebugOptionsVisibility(boolean visible) { 546 if (mEnableDebugMenuOptions != visible) { 547 mEnableDebugMenuOptions = visible; 548 invalidateOptionsMenu(); 549 } 550 } 551 552 /** 553 * Updates the fragment/view visibility according to the current mode, such as 554 * {@link ActionBarAdapter#isSearchMode()} and {@link ActionBarAdapter#getCurrentTab()}. 555 */ 556 private void updateFragmentsVisibility() { 557 int tab = mActionBarAdapter.getCurrentTab(); 558 559 if (mActionBarAdapter.isSearchMode()) { 560 mTabPagerAdapter.setSearchMode(true); 561 } else { 562 // No smooth scrolling if quitting from the search mode. 563 final boolean wasSearchMode = mTabPagerAdapter.isSearchMode(); 564 mTabPagerAdapter.setSearchMode(false); 565 if (mTabPager.getCurrentItem() != tab) { 566 mTabPager.setCurrentItem(tab, !wasSearchMode); 567 } 568 } 569 invalidateOptionsMenu(); 570 showEmptyStateForTab(tab); 571 } 572 573 private void showEmptyStateForTab(int tab) { 574 if (mContactsUnavailableFragment != null) { 575 switch (tab) { 576 case TabState.FAVORITES: 577 mContactsUnavailableFragment.setMessageText( 578 R.string.listTotalAllContactsZeroStarred, -1); 579 break; 580 case TabState.ALL: 581 mContactsUnavailableFragment.setMessageText(R.string.noContacts, -1); 582 break; 583 } 584 // When using the mContactsUnavailableFragment the ViewPager doesn't contain two views. 585 // Therefore, we have to trick the ViewPagerTabs into thinking we have changed tabs 586 // when the mContactsUnavailableFragment changes. Otherwise the tab strip won't move. 587 mViewPagerTabs.onPageScrolled(tab, 0, 0); 588 } 589 } 590 591 private class TabPagerListener implements ViewPager.OnPageChangeListener { 592 593 // This package-protected constructor is here because of a possible compiler bug. 594 // PeopleActivity$1.class should be generated due to the private outer/inner class access 595 // needed here. But for some reason, PeopleActivity$1.class is missing. 596 // Since $1 class is needed as a jvm work around to get access to the inner class, 597 // changing the constructor to package-protected or public will solve the problem. 598 // To verify whether $1 class is needed, javap PeopleActivity$TabPagerListener and look for 599 // references to PeopleActivity$1. 600 // 601 // When the constructor is private and PeopleActivity$1.class is missing, proguard will 602 // correctly catch this and throw warnings and error out the build on user/userdebug builds. 603 // 604 // All private inner classes below also need this fix. 605 TabPagerListener() {} 606 607 @Override 608 public void onPageScrollStateChanged(int state) { 609 if (!mTabPagerAdapter.isSearchMode()) { 610 mViewPagerTabs.onPageScrollStateChanged(state); 611 } 612 } 613 614 @Override 615 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 616 if (!mTabPagerAdapter.isSearchMode()) { 617 mViewPagerTabs.onPageScrolled(position, positionOffset, positionOffsetPixels); 618 } 619 } 620 621 @Override 622 public void onPageSelected(int position) { 623 // Make sure not in the search mode, in which case position != TabState.ordinal(). 624 if (!mTabPagerAdapter.isSearchMode()) { 625 mActionBarAdapter.setCurrentTab(position, false); 626 mViewPagerTabs.onPageSelected(position); 627 showEmptyStateForTab(position); 628 invalidateOptionsMenu(); 629 } 630 } 631 } 632 633 /** 634 * Adapter for the {@link ViewPager}. Unlike {@link FragmentPagerAdapter}, 635 * {@link #instantiateItem} returns existing fragments, and {@link #instantiateItem}/ 636 * {@link #destroyItem} show/hide fragments instead of attaching/detaching. 637 * 638 * In search mode, we always show the "all" fragment, and disable the swipe. We change the 639 * number of items to 1 to disable the swipe. 640 * 641 * TODO figure out a more straight way to disable swipe. 642 */ 643 private class TabPagerAdapter extends PagerAdapter { 644 private final FragmentManager mFragmentManager; 645 private FragmentTransaction mCurTransaction = null; 646 647 private boolean mTabPagerAdapterSearchMode; 648 649 private Fragment mCurrentPrimaryItem; 650 651 public TabPagerAdapter() { 652 mFragmentManager = getFragmentManager(); 653 } 654 655 public boolean isSearchMode() { 656 return mTabPagerAdapterSearchMode; 657 } 658 659 public void setSearchMode(boolean searchMode) { 660 if (searchMode == mTabPagerAdapterSearchMode) { 661 return; 662 } 663 mTabPagerAdapterSearchMode = searchMode; 664 notifyDataSetChanged(); 665 } 666 667 @Override 668 public int getCount() { 669 return mTabPagerAdapterSearchMode ? 1 : TabState.COUNT; 670 } 671 672 /** Gets called when the number of items changes. */ 673 @Override 674 public int getItemPosition(Object object) { 675 if (mTabPagerAdapterSearchMode) { 676 if (object == mAllFragment) { 677 return 0; // Only 1 page in search mode 678 } 679 } else { 680 if (object == mFavoritesFragment) { 681 return getTabPositionForTextDirection(TabState.FAVORITES); 682 } 683 if (object == mAllFragment) { 684 return getTabPositionForTextDirection(TabState.ALL); 685 } 686 } 687 return POSITION_NONE; 688 } 689 690 @Override 691 public void startUpdate(ViewGroup container) { 692 } 693 694 private Fragment getFragment(int position) { 695 position = getTabPositionForTextDirection(position); 696 if (mTabPagerAdapterSearchMode) { 697 if (position != 0) { 698 // This has only been observed in monkey tests. 699 // Let's log this issue, but not crash 700 Log.w(TAG, "Request fragment at position=" + position + ", eventhough we " + 701 "are in search mode"); 702 } 703 return mAllFragment; 704 } else { 705 if (position == TabState.FAVORITES) { 706 return mFavoritesFragment; 707 } else if (position == TabState.ALL) { 708 return mAllFragment; 709 } 710 } 711 throw new IllegalArgumentException("position: " + position); 712 } 713 714 @Override 715 public Object instantiateItem(ViewGroup container, int position) { 716 if (mCurTransaction == null) { 717 mCurTransaction = mFragmentManager.beginTransaction(); 718 } 719 Fragment f = getFragment(position); 720 mCurTransaction.show(f); 721 722 // Non primary pages are not visible. 723 f.setUserVisibleHint(f == mCurrentPrimaryItem); 724 return f; 725 } 726 727 @Override 728 public void destroyItem(ViewGroup container, int position, Object object) { 729 if (mCurTransaction == null) { 730 mCurTransaction = mFragmentManager.beginTransaction(); 731 } 732 mCurTransaction.hide((Fragment) object); 733 } 734 735 @Override 736 public void finishUpdate(ViewGroup container) { 737 if (mCurTransaction != null) { 738 mCurTransaction.commitAllowingStateLoss(); 739 mCurTransaction = null; 740 mFragmentManager.executePendingTransactions(); 741 } 742 } 743 744 @Override 745 public boolean isViewFromObject(View view, Object object) { 746 return ((Fragment) object).getView() == view; 747 } 748 749 @Override 750 public void setPrimaryItem(ViewGroup container, int position, Object object) { 751 Fragment fragment = (Fragment) object; 752 if (mCurrentPrimaryItem != fragment) { 753 if (mCurrentPrimaryItem != null) { 754 mCurrentPrimaryItem.setUserVisibleHint(false); 755 } 756 if (fragment != null) { 757 fragment.setUserVisibleHint(true); 758 } 759 mCurrentPrimaryItem = fragment; 760 } 761 } 762 763 @Override 764 public Parcelable saveState() { 765 return null; 766 } 767 768 @Override 769 public void restoreState(Parcelable state, ClassLoader loader) { 770 } 771 772 @Override 773 public CharSequence getPageTitle(int position) { 774 return mTabTitles[position]; 775 } 776 } 777 778 private void setQueryTextToFragment(String query) { 779 mAllFragment.setQueryString(query, true); 780 mAllFragment.setVisibleScrollbarEnabled(!mAllFragment.isSearchMode()); 781 } 782 783 private void configureContactListFragmentForRequest() { 784 Uri contactUri = mRequest.getContactUri(); 785 if (contactUri != null) { 786 mAllFragment.setSelectedContactUri(contactUri); 787 } 788 789 mAllFragment.setFilter(mContactListFilterController.getFilter()); 790 setQueryTextToFragment(mActionBarAdapter.getQueryString()); 791 792 if (mRequest.isDirectorySearchEnabled()) { 793 mAllFragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_DEFAULT); 794 } else { 795 mAllFragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE); 796 } 797 } 798 799 private void configureContactListFragment() { 800 // Filter may be changed when this Activity is in background. 801 mAllFragment.setFilter(mContactListFilterController.getFilter()); 802 803 mAllFragment.setVerticalScrollbarPosition(getScrollBarPosition()); 804 mAllFragment.setSelectionVisible(false); 805 } 806 807 private int getScrollBarPosition() { 808 return isRTL() ? View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT; 809 } 810 811 private boolean isRTL() { 812 final Locale locale = Locale.getDefault(); 813 return TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL; 814 } 815 816 @Override 817 public void onProviderStatusChange() { 818 updateViewConfiguration(false); 819 } 820 821 private void updateViewConfiguration(boolean forceUpdate) { 822 ProviderStatusWatcher.Status providerStatus = mProviderStatusWatcher.getProviderStatus(); 823 if (!forceUpdate && (mProviderStatus != null) 824 && (providerStatus.status == mProviderStatus.status)) return; 825 mProviderStatus = providerStatus; 826 827 View contactsUnavailableView = findViewById(R.id.contacts_unavailable_view); 828 829 if (mProviderStatus.status == ProviderStatus.STATUS_NORMAL) { 830 // Ensure that the mTabPager is visible; we may have made it invisible below. 831 contactsUnavailableView.setVisibility(View.GONE); 832 if (mTabPager != null) { 833 mTabPager.setVisibility(View.VISIBLE); 834 } 835 836 if (mAllFragment != null) { 837 mAllFragment.setEnabled(true); 838 } 839 } else { 840 // If there are no accounts on the device and we should show the "no account" prompt 841 // (based on {@link SharedPreferences}), then launch the account setup activity so the 842 // user can sign-in or create an account. 843 // 844 // Also check for ability to modify accounts. In limited user mode, you can't modify 845 // accounts so there is no point sending users to account setup activity. 846 final UserManager userManager = UserManager.get(this); 847 final boolean disallowModifyAccounts = userManager.getUserRestrictions().getBoolean( 848 UserManager.DISALLOW_MODIFY_ACCOUNTS); 849 if (!disallowModifyAccounts && !areContactWritableAccountsAvailable() && 850 AccountPromptUtils.shouldShowAccountPrompt(this)) { 851 AccountPromptUtils.neverShowAccountPromptAgain(this); 852 AccountPromptUtils.launchAccountPrompt(this); 853 return; 854 } 855 856 // Otherwise, continue setting up the page so that the user can still use the app 857 // without an account. 858 if (mAllFragment != null) { 859 mAllFragment.setEnabled(false); 860 } 861 if (mContactsUnavailableFragment == null) { 862 mContactsUnavailableFragment = new ContactsUnavailableFragment(); 863 mContactsUnavailableFragment.setOnContactsUnavailableActionListener( 864 new ContactsUnavailableFragmentListener()); 865 getFragmentManager().beginTransaction() 866 .replace(R.id.contacts_unavailable_container, mContactsUnavailableFragment) 867 .commitAllowingStateLoss(); 868 } 869 mContactsUnavailableFragment.updateStatus(mProviderStatus); 870 871 // Show the contactsUnavailableView, and hide the mTabPager so that we don't 872 // see it sliding in underneath the contactsUnavailableView at the edges. 873 contactsUnavailableView.setVisibility(View.VISIBLE); 874 if (mTabPager != null) { 875 mTabPager.setVisibility(View.GONE); 876 } 877 878 showEmptyStateForTab(mActionBarAdapter.getCurrentTab()); 879 } 880 881 invalidateOptionsMenuIfNeeded(); 882 } 883 884 private final class ContactBrowserActionListener implements OnContactBrowserActionListener { 885 ContactBrowserActionListener() {} 886 887 @Override 888 public void onSelectionChange() { 889 890 } 891 892 @Override 893 public void onViewContactAction(Uri contactLookupUri) { 894 Intent intent = QuickContact.composeQuickContactsIntent(PeopleActivity.this, 895 (Rect) null, contactLookupUri, QuickContactActivity.MODE_FULLY_EXPANDED, null); 896 startActivity(intent); 897 } 898 899 @Override 900 public void onDeleteContactAction(Uri contactUri) { 901 ContactDeletionInteraction.start(PeopleActivity.this, contactUri, false); 902 } 903 904 @Override 905 public void onFinishAction() { 906 onBackPressed(); 907 } 908 909 @Override 910 public void onInvalidSelection() { 911 ContactListFilter filter; 912 ContactListFilter currentFilter = mAllFragment.getFilter(); 913 if (currentFilter != null 914 && currentFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { 915 filter = ContactListFilter.createFilterWithType( 916 ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS); 917 mAllFragment.setFilter(filter); 918 } else { 919 filter = ContactListFilter.createFilterWithType( 920 ContactListFilter.FILTER_TYPE_SINGLE_CONTACT); 921 mAllFragment.setFilter(filter, false); 922 } 923 mContactListFilterController.setContactListFilter(filter, true); 924 } 925 } 926 927 private class ContactsUnavailableFragmentListener 928 implements OnContactsUnavailableActionListener { 929 ContactsUnavailableFragmentListener() {} 930 931 @Override 932 public void onCreateNewContactAction() { 933 startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI)); 934 } 935 936 @Override 937 public void onAddAccountAction() { 938 Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT); 939 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 940 intent.putExtra(Settings.EXTRA_AUTHORITIES, 941 new String[] { ContactsContract.AUTHORITY }); 942 startActivity(intent); 943 } 944 945 @Override 946 public void onImportContactsFromFileAction() { 947 ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable(), 948 PeopleActivity.class); 949 } 950 951 @Override 952 public void onFreeInternalStorageAction() { 953 startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS)); 954 } 955 } 956 957 private final class StrequentContactListFragmentListener 958 implements ContactTileListFragment.Listener { 959 StrequentContactListFragmentListener() {} 960 961 @Override 962 public void onContactSelected(Uri contactUri, Rect targetRect) { 963 Intent intent = QuickContact.composeQuickContactsIntent(PeopleActivity.this, 964 targetRect, contactUri, QuickContactActivity.MODE_FULLY_EXPANDED, null); 965 startActivity(intent); 966 } 967 968 @Override 969 public void onCallNumberDirectly(String phoneNumber) { 970 // No need to call phone number directly from People app. 971 Log.w(TAG, "unexpected invocation of onCallNumberDirectly()"); 972 } 973 } 974 975 @Override 976 public boolean onCreateOptionsMenu(Menu menu) { 977 if (!areContactsAvailable()) { 978 // If contacts aren't available, hide all menu items. 979 return false; 980 } 981 super.onCreateOptionsMenu(menu); 982 983 MenuInflater inflater = getMenuInflater(); 984 inflater.inflate(R.menu.people_options, menu); 985 986 return true; 987 } 988 989 private void invalidateOptionsMenuIfNeeded() { 990 if (isOptionsMenuChanged()) { 991 invalidateOptionsMenu(); 992 } 993 } 994 995 public boolean isOptionsMenuChanged() { 996 if (mOptionsMenuContactsAvailable != areContactsAvailable()) { 997 return true; 998 } 999 1000 if (mAllFragment != null && mAllFragment.isOptionsMenuChanged()) { 1001 return true; 1002 } 1003 1004 return false; 1005 } 1006 1007 @Override 1008 public boolean onPrepareOptionsMenu(Menu menu) { 1009 mOptionsMenuContactsAvailable = areContactsAvailable(); 1010 if (!mOptionsMenuContactsAvailable) { 1011 return false; 1012 } 1013 1014 // Get references to individual menu items in the menu 1015 final MenuItem contactsFilterMenu = menu.findItem(R.id.menu_contacts_filter); 1016 final MenuItem clearFrequentsMenu = menu.findItem(R.id.menu_clear_frequents); 1017 final MenuItem helpMenu = menu.findItem(R.id.menu_help); 1018 1019 final boolean isSearchMode = mActionBarAdapter.isSearchMode(); 1020 if (isSearchMode) { 1021 contactsFilterMenu.setVisible(false); 1022 clearFrequentsMenu.setVisible(false); 1023 helpMenu.setVisible(false); 1024 } else { 1025 switch (mActionBarAdapter.getCurrentTab()) { 1026 case TabState.FAVORITES: 1027 contactsFilterMenu.setVisible(false); 1028 clearFrequentsMenu.setVisible(hasFrequents()); 1029 break; 1030 case TabState.ALL: 1031 contactsFilterMenu.setVisible(true); 1032 clearFrequentsMenu.setVisible(false); 1033 break; 1034 } 1035 HelpUtils.prepareHelpMenuItem(this, helpMenu, R.string.help_url_people_main); 1036 } 1037 final boolean showMiscOptions = !isSearchMode; 1038 makeMenuItemVisible(menu, R.id.menu_search, showMiscOptions); 1039 makeMenuItemVisible(menu, R.id.menu_import_export, showMiscOptions); 1040 makeMenuItemVisible(menu, R.id.menu_accounts, showMiscOptions); 1041 makeMenuItemVisible(menu, R.id.menu_settings, 1042 showMiscOptions && !ContactsPreferenceActivity.isEmpty(this)); 1043 1044 // Debug options need to be visible even in search mode. 1045 makeMenuItemVisible(menu, R.id.export_database, mEnableDebugMenuOptions); 1046 1047 return true; 1048 } 1049 1050 /** 1051 * Returns whether there are any frequently contacted people being displayed 1052 * @return 1053 */ 1054 private boolean hasFrequents() { 1055 return mFavoritesFragment.hasFrequents(); 1056 } 1057 1058 private void makeMenuItemVisible(Menu menu, int itemId, boolean visible) { 1059 MenuItem item =menu.findItem(itemId); 1060 if (item != null) { 1061 item.setVisible(visible); 1062 } 1063 } 1064 1065 @Override 1066 public boolean onOptionsItemSelected(MenuItem item) { 1067 if (mDisableOptionItemSelected) { 1068 return false; 1069 } 1070 1071 switch (item.getItemId()) { 1072 case android.R.id.home: { 1073 // The home icon on the action bar is pressed 1074 if (mActionBarAdapter.isUpShowing()) { 1075 // "UP" icon press -- should be treated as "back". 1076 onBackPressed(); 1077 } 1078 return true; 1079 } 1080 case R.id.menu_settings: { 1081 final Intent intent = new Intent(this, ContactsPreferenceActivity.class); 1082 // Since there is only one section right now, make sure it is selected on 1083 // small screens. 1084 intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, 1085 DisplayOptionsPreferenceFragment.class.getName()); 1086 // By default, the title of the activity should be equivalent to the fragment 1087 // title. We set this argument to avoid this. Because of a bug, the following 1088 // line isn't necessary. But, once the bug is fixed this may become necessary. 1089 // b/5045558 refers to this issue, as well as another. 1090 intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE, 1091 R.string.activity_title_settings); 1092 startActivity(intent); 1093 return true; 1094 } 1095 case R.id.menu_contacts_filter: { 1096 AccountFilterUtil.startAccountFilterActivityForResult( 1097 this, SUBACTIVITY_ACCOUNT_FILTER, 1098 mContactListFilterController.getFilter()); 1099 return true; 1100 } 1101 case R.id.menu_search: { 1102 onSearchRequested(); 1103 return true; 1104 } 1105 case R.id.menu_import_export: { 1106 ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable(), 1107 PeopleActivity.class); 1108 return true; 1109 } 1110 case R.id.menu_clear_frequents: { 1111 ClearFrequentsDialog.show(getFragmentManager()); 1112 return true; 1113 } 1114 case R.id.menu_accounts: { 1115 final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS); 1116 intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] { 1117 ContactsContract.AUTHORITY 1118 }); 1119 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 1120 startActivity(intent); 1121 return true; 1122 } 1123 case R.id.export_database: { 1124 final Intent intent = new Intent("com.android.providers.contacts.DUMP_DATABASE"); 1125 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 1126 startActivity(intent); 1127 return true; 1128 } 1129 } 1130 return false; 1131 } 1132 1133 @Override 1134 public boolean onSearchRequested() { // Search key pressed. 1135 mActionBarAdapter.setSearchMode(true); 1136 return true; 1137 } 1138 1139 @Override 1140 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 1141 switch (requestCode) { 1142 case SUBACTIVITY_ACCOUNT_FILTER: { 1143 AccountFilterUtil.handleAccountFilterResult( 1144 mContactListFilterController, resultCode, data); 1145 break; 1146 } 1147 1148 // TODO: Using the new startActivityWithResultFromFragment API this should not be needed 1149 // anymore 1150 case ContactEntryListFragment.ACTIVITY_REQUEST_CODE_PICKER: 1151 if (resultCode == RESULT_OK) { 1152 mAllFragment.onPickerResult(data); 1153 } 1154 1155 // TODO fix or remove multipicker code 1156 // else if (resultCode == RESULT_CANCELED && mMode == MODE_PICK_MULTIPLE_PHONES) { 1157 // // Finish the activity if the sub activity was canceled as back key is used 1158 // // to confirm user selection in MODE_PICK_MULTIPLE_PHONES. 1159 // finish(); 1160 // } 1161 // break; 1162 } 1163 } 1164 1165 @Override 1166 public boolean onKeyDown(int keyCode, KeyEvent event) { 1167 // TODO move to the fragment 1168 switch (keyCode) { 1169 // case KeyEvent.KEYCODE_CALL: { 1170 // if (callSelection()) { 1171 // return true; 1172 // } 1173 // break; 1174 // } 1175 1176 case KeyEvent.KEYCODE_DEL: { 1177 if (deleteSelection()) { 1178 return true; 1179 } 1180 break; 1181 } 1182 default: { 1183 // Bring up the search UI if the user starts typing 1184 final int unicodeChar = event.getUnicodeChar(); 1185 if ((unicodeChar != 0) 1186 // If COMBINING_ACCENT is set, it's not a unicode character. 1187 && ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) == 0) 1188 && !Character.isWhitespace(unicodeChar)) { 1189 String query = new String(new int[]{ unicodeChar }, 0, 1); 1190 if (!mActionBarAdapter.isSearchMode()) { 1191 mActionBarAdapter.setQueryString(query); 1192 mActionBarAdapter.setSearchMode(true); 1193 return true; 1194 } 1195 } 1196 } 1197 } 1198 1199 return super.onKeyDown(keyCode, event); 1200 } 1201 1202 @Override 1203 public void onBackPressed() { 1204 if (mActionBarAdapter.isSearchMode()) { 1205 mActionBarAdapter.setSearchMode(false); 1206 } else { 1207 super.onBackPressed(); 1208 } 1209 } 1210 1211 private boolean deleteSelection() { 1212 // TODO move to the fragment 1213 // if (mActionCode == ContactsRequest.ACTION_DEFAULT) { 1214 // final int position = mListView.getSelectedItemPosition(); 1215 // if (position != ListView.INVALID_POSITION) { 1216 // Uri contactUri = getContactUri(position); 1217 // if (contactUri != null) { 1218 // doContactDelete(contactUri); 1219 // return true; 1220 // } 1221 // } 1222 // } 1223 return false; 1224 } 1225 1226 @Override 1227 protected void onSaveInstanceState(Bundle outState) { 1228 super.onSaveInstanceState(outState); 1229 mActionBarAdapter.onSaveInstanceState(outState); 1230 1231 // Clear the listener to make sure we don't get callbacks after onSaveInstanceState, 1232 // in order to avoid doing fragment transactions after it. 1233 // TODO Figure out a better way to deal with the issue. 1234 mDisableOptionItemSelected = true; 1235 mActionBarAdapter.setListener(null); 1236 if (mTabPager != null) { 1237 mTabPager.setOnPageChangeListener(null); 1238 } 1239 } 1240 1241 @Override 1242 protected void onRestoreInstanceState(Bundle savedInstanceState) { 1243 super.onRestoreInstanceState(savedInstanceState); 1244 // In our own lifecycle, the focus is saved and restore but later taken away by the 1245 // ViewPager. As a hack, we force focus on the SearchView if we know that we are searching. 1246 // This fixes the keyboard going away on screen rotation 1247 if (mActionBarAdapter.isSearchMode()) { 1248 mActionBarAdapter.setFocusOnSearchView(); 1249 } 1250 } 1251 1252 @Override 1253 public DialogManager getDialogManager() { 1254 return mDialogManager; 1255 } 1256 1257 @Override 1258 public void onClick(View view) { 1259 switch (view.getId()) { 1260 case R.id.floating_action_button: 1261 Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 1262 Bundle extras = getIntent().getExtras(); 1263 if (extras != null) { 1264 intent.putExtras(extras); 1265 } 1266 startActivity(intent); 1267 break; 1268 default: 1269 Log.wtf(TAG, "Unexpected onClick event from " + view); 1270 } 1271 } 1272 1273 /** 1274 * Returns the tab position adjusted for the text direction. 1275 */ 1276 private int getTabPositionForTextDirection(int position) { 1277 if (isRTL()) { 1278 return TabState.COUNT - 1 - position; 1279 } 1280 return position; 1281 } 1282 } 1283