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