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.ActivityNotFoundException; 23 import android.content.ContentValues; 24 import android.content.Intent; 25 import android.graphics.Rect; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.Parcelable; 30 import android.os.UserManager; 31 import android.preference.PreferenceActivity; 32 import android.provider.ContactsContract; 33 import android.provider.ContactsContract.Contacts; 34 import android.provider.ContactsContract.ProviderStatus; 35 import android.provider.ContactsContract.QuickContact; 36 import android.provider.Settings; 37 import android.support.v13.app.FragmentPagerAdapter; 38 import android.support.v4.view.PagerAdapter; 39 import android.support.v4.view.ViewPager; 40 import android.text.TextUtils; 41 import android.util.Log; 42 import android.view.KeyCharacterMap; 43 import android.view.KeyEvent; 44 import android.view.Menu; 45 import android.view.MenuInflater; 46 import android.view.MenuItem; 47 import android.view.MenuItem.OnMenuItemClickListener; 48 import android.view.View; 49 import android.view.ViewGroup; 50 import android.widget.Toast; 51 52 import com.android.contacts.ContactSaveService; 53 import com.android.contacts.ContactsActivity; 54 import com.android.contacts.ContactsUtils; 55 import com.android.contacts.R; 56 import com.android.contacts.activities.ActionBarAdapter.TabState; 57 import com.android.contacts.detail.ContactDetailFragment; 58 import com.android.contacts.detail.ContactDetailLayoutController; 59 import com.android.contacts.detail.ContactDetailUpdatesFragment; 60 import com.android.contacts.detail.ContactLoaderFragment; 61 import com.android.contacts.detail.ContactLoaderFragment.ContactLoaderFragmentListener; 62 import com.android.contacts.common.dialog.ClearFrequentsDialog; 63 import com.android.contacts.group.GroupBrowseListFragment; 64 import com.android.contacts.group.GroupBrowseListFragment.OnGroupBrowserActionListener; 65 import com.android.contacts.group.GroupDetailFragment; 66 import com.android.contacts.interactions.ContactDeletionInteraction; 67 import com.android.contacts.common.interactions.ImportExportDialogFragment; 68 import com.android.contacts.list.ContactBrowseListFragment; 69 import com.android.contacts.common.list.ContactEntryListFragment; 70 import com.android.contacts.common.list.ContactListFilter; 71 import com.android.contacts.common.list.ContactListFilterController; 72 import com.android.contacts.common.list.ContactTileAdapter.DisplayType; 73 import com.android.contacts.list.ContactTileFrequentFragment; 74 import com.android.contacts.list.ContactTileListFragment; 75 import com.android.contacts.list.ContactsIntentResolver; 76 import com.android.contacts.list.ContactsRequest; 77 import com.android.contacts.list.ContactsUnavailableFragment; 78 import com.android.contacts.list.DefaultContactBrowseListFragment; 79 import com.android.contacts.common.list.DirectoryListLoader; 80 import com.android.contacts.list.OnContactBrowserActionListener; 81 import com.android.contacts.list.OnContactsUnavailableActionListener; 82 import com.android.contacts.list.ProviderStatusWatcher; 83 import com.android.contacts.list.ProviderStatusWatcher.ProviderStatusListener; 84 import com.android.contacts.model.Contact; 85 import com.android.contacts.common.model.account.AccountWithDataSet; 86 import com.android.contacts.preference.ContactsPreferenceActivity; 87 import com.android.contacts.preference.DisplayOptionsPreferenceFragment; 88 import com.android.contacts.common.util.AccountFilterUtil; 89 import com.android.contacts.util.AccountPromptUtils; 90 import com.android.contacts.common.util.Constants; 91 import com.android.contacts.util.DialogManager; 92 import com.android.contacts.util.HelpUtils; 93 import com.android.contacts.util.PhoneCapabilityTester; 94 import com.android.contacts.common.util.UriUtils; 95 import com.android.contacts.widget.TransitionAnimationView; 96 97 import java.util.ArrayList; 98 import java.util.Locale; 99 import java.util.concurrent.atomic.AtomicInteger; 100 101 /** 102 * Displays a list to browse contacts. For xlarge screens, this also displays a detail-pane on 103 * the right. 104 */ 105 public class PeopleActivity extends ContactsActivity 106 implements View.OnCreateContextMenuListener, ActionBarAdapter.Listener, 107 DialogManager.DialogShowingViewActivity, 108 ContactListFilterController.ContactListFilterListener, ProviderStatusListener { 109 110 private static final String TAG = "PeopleActivity"; 111 112 private static final int TAB_FADE_IN_DURATION = 500; 113 114 private static final String ENABLE_DEBUG_OPTIONS_HIDDEN_CODE = "debug debug!"; 115 116 // These values needs to start at 2. See {@link ContactEntryListFragment}. 117 private static final int SUBACTIVITY_NEW_CONTACT = 2; 118 private static final int SUBACTIVITY_EDIT_CONTACT = 3; 119 private static final int SUBACTIVITY_NEW_GROUP = 4; 120 private static final int SUBACTIVITY_EDIT_GROUP = 5; 121 private static final int SUBACTIVITY_ACCOUNT_FILTER = 6; 122 123 private final DialogManager mDialogManager = new DialogManager(this); 124 125 private ContactsIntentResolver mIntentResolver; 126 private ContactsRequest mRequest; 127 128 private ActionBarAdapter mActionBarAdapter; 129 130 private ContactDetailFragment mContactDetailFragment; 131 132 private ContactLoaderFragment mContactDetailLoaderFragment; 133 private final ContactDetailLoaderFragmentListener mContactDetailLoaderFragmentListener = 134 new ContactDetailLoaderFragmentListener(); 135 136 private GroupDetailFragment mGroupDetailFragment; 137 private final GroupDetailFragmentListener mGroupDetailFragmentListener = 138 new GroupDetailFragmentListener(); 139 140 private ContactTileListFragment.Listener mFavoritesFragmentListener = 141 new StrequentContactListFragmentListener(); 142 143 private ContactListFilterController mContactListFilterController; 144 145 private ContactsUnavailableFragment mContactsUnavailableFragment; 146 private ProviderStatusWatcher mProviderStatusWatcher; 147 private ProviderStatusWatcher.Status mProviderStatus; 148 149 private boolean mOptionsMenuContactsAvailable; 150 151 /** 152 * Showing a list of Contacts. Also used for showing search results in search mode. 153 */ 154 private DefaultContactBrowseListFragment mAllFragment; 155 private ContactTileListFragment mFavoritesFragment; 156 private ContactTileFrequentFragment mFrequentFragment; 157 private GroupBrowseListFragment mGroupsFragment; 158 159 private View mFavoritesView; 160 private View mBrowserView; 161 private TransitionAnimationView mPeopleActivityView; 162 private TransitionAnimationView mContactDetailsView; 163 private TransitionAnimationView mGroupDetailsView; 164 165 /** ViewPager for swipe, used only on the phone (i.e. one-pane mode) */ 166 private ViewPager mTabPager; 167 private TabPagerAdapter mTabPagerAdapter; 168 private final TabPagerListener mTabPagerListener = new TabPagerListener(); 169 170 private ContactDetailLayoutController mContactDetailLayoutController; 171 172 private boolean mEnableDebugMenuOptions; 173 174 private final Handler mHandler = new Handler(); 175 176 /** 177 * True if this activity instance is a re-created one. i.e. set true after orientation change. 178 * This is set in {@link #onCreate} for later use in {@link #onStart}. 179 */ 180 private boolean mIsRecreatedInstance; 181 182 /** 183 * If {@link #configureFragments(boolean)} is already called. Used to avoid calling it twice 184 * in {@link #onStart}. 185 * (This initialization only needs to be done once in onStart() when the Activity was just 186 * created from scratch -- i.e. onCreate() was just called) 187 */ 188 private boolean mFragmentInitialized; 189 190 /** 191 * Whether or not the current contact filter is valid or not. We need to do a check on 192 * start of the app to verify that the user is not in single contact mode. If so, we should 193 * dynamically change the filter, unless the incoming intent specifically requested a contact 194 * that should be displayed in that mode. 195 */ 196 private boolean mCurrentFilterIsValid; 197 198 /** 199 * This is to disable {@link #onOptionsItemSelected} when we trying to stop the activity. 200 */ 201 private boolean mDisableOptionItemSelected; 202 203 /** Sequential ID assigned to each instance; used for logging */ 204 private final int mInstanceId; 205 private static final AtomicInteger sNextInstanceId = new AtomicInteger(); 206 207 public PeopleActivity() { 208 mInstanceId = sNextInstanceId.getAndIncrement(); 209 mIntentResolver = new ContactsIntentResolver(this); 210 mProviderStatusWatcher = ProviderStatusWatcher.getInstance(this); 211 } 212 213 @Override 214 public String toString() { 215 // Shown on logcat 216 return String.format("%s@%d", getClass().getSimpleName(), mInstanceId); 217 } 218 219 public boolean areContactsAvailable() { 220 return (mProviderStatus != null) 221 && mProviderStatus.status == ProviderStatus.STATUS_NORMAL; 222 } 223 224 private boolean areContactWritableAccountsAvailable() { 225 return ContactsUtils.areContactWritableAccountsAvailable(this); 226 } 227 228 private boolean areGroupWritableAccountsAvailable() { 229 return ContactsUtils.areGroupWritableAccountsAvailable(this); 230 } 231 232 /** 233 * Initialize fragments that are (or may not be) in the layout. 234 * 235 * For the fragments that are in the layout, we initialize them in 236 * {@link #createViewsAndFragments(Bundle)} after inflating the layout. 237 * 238 * However, there are special fragments which may not be in the layout, so we have to do the 239 * initialization here. 240 * The target fragments are: 241 * - {@link ContactDetailFragment} and {@link ContactDetailUpdatesFragment}: They may not be 242 * in the layout depending on the configuration. (i.e. portrait) 243 * - {@link ContactsUnavailableFragment}: We always create it at runtime. 244 */ 245 @Override 246 public void onAttachFragment(Fragment fragment) { 247 if (fragment instanceof ContactDetailFragment) { 248 mContactDetailFragment = (ContactDetailFragment) fragment; 249 } else if (fragment instanceof ContactsUnavailableFragment) { 250 mContactsUnavailableFragment = (ContactsUnavailableFragment)fragment; 251 mContactsUnavailableFragment.setOnContactsUnavailableActionListener( 252 new ContactsUnavailableFragmentListener()); 253 } 254 } 255 256 @Override 257 protected void onCreate(Bundle savedState) { 258 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 259 Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate start"); 260 } 261 super.onCreate(savedState); 262 263 if (!processIntent(false)) { 264 finish(); 265 return; 266 } 267 mContactListFilterController = ContactListFilterController.getInstance(this); 268 mContactListFilterController.checkFilterValidity(false); 269 mContactListFilterController.addListener(this); 270 271 mProviderStatusWatcher.addListener(this); 272 273 mIsRecreatedInstance = (savedState != null); 274 createViewsAndFragments(savedState); 275 getWindow().setBackgroundDrawableResource(R.color.background_primary); 276 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 277 Log.d(Constants.PERFORMANCE_TAG, "PeopleActivity.onCreate finish"); 278 } 279 } 280 281 @Override 282 protected void onNewIntent(Intent intent) { 283 setIntent(intent); 284 if (!processIntent(true)) { 285 finish(); 286 return; 287 } 288 mActionBarAdapter.initialize(null, mRequest); 289 290 mContactListFilterController.checkFilterValidity(false); 291 mCurrentFilterIsValid = true; 292 293 // Re-configure fragments. 294 configureFragments(true /* from request */); 295 invalidateOptionsMenuIfNeeded(); 296 } 297 298 /** 299 * Resolve the intent and initialize {@link #mRequest}, and launch another activity if redirect 300 * is needed. 301 * 302 * @param forNewIntent set true if it's called from {@link #onNewIntent(Intent)}. 303 * @return {@code true} if {@link PeopleActivity} should continue running. {@code false} 304 * if it shouldn't, in which case the caller should finish() itself and shouldn't do 305 * farther initialization. 306 */ 307 private boolean processIntent(boolean forNewIntent) { 308 // Extract relevant information from the intent 309 mRequest = mIntentResolver.resolveIntent(getIntent()); 310 if (Log.isLoggable(TAG, Log.DEBUG)) { 311 Log.d(TAG, this + " processIntent: forNewIntent=" + forNewIntent 312 + " intent=" + getIntent() + " request=" + mRequest); 313 } 314 if (!mRequest.isValid()) { 315 setResult(RESULT_CANCELED); 316 return false; 317 } 318 319 Intent redirect = mRequest.getRedirectIntent(); 320 if (redirect != null) { 321 // Need to start a different activity 322 startActivity(redirect); 323 return false; 324 } 325 326 if (mRequest.getActionCode() == ContactsRequest.ACTION_VIEW_CONTACT 327 && !PhoneCapabilityTester.isUsingTwoPanes(this)) { 328 redirect = new Intent(this, ContactDetailActivity.class); 329 redirect.setAction(Intent.ACTION_VIEW); 330 redirect.setData(mRequest.getContactUri()); 331 startActivity(redirect); 332 return false; 333 } 334 return true; 335 } 336 337 private void createViewsAndFragments(Bundle savedState) { 338 setContentView(R.layout.people_activity); 339 340 final FragmentManager fragmentManager = getFragmentManager(); 341 342 // Hide all tabs (the current tab will later be reshown once a tab is selected) 343 final FragmentTransaction transaction = fragmentManager.beginTransaction(); 344 345 // Prepare the fragments which are used both on 1-pane and on 2-pane. 346 final boolean isUsingTwoPanes = PhoneCapabilityTester.isUsingTwoPanes(this); 347 if (isUsingTwoPanes) { 348 mFavoritesFragment = getFragment(R.id.favorites_fragment); 349 mAllFragment = getFragment(R.id.all_fragment); 350 mGroupsFragment = getFragment(R.id.groups_fragment); 351 } else { 352 mTabPager = getView(R.id.tab_pager); 353 mTabPagerAdapter = new TabPagerAdapter(); 354 mTabPager.setAdapter(mTabPagerAdapter); 355 mTabPager.setOnPageChangeListener(mTabPagerListener); 356 357 final String FAVORITE_TAG = "tab-pager-favorite"; 358 final String ALL_TAG = "tab-pager-all"; 359 final String GROUPS_TAG = "tab-pager-groups"; 360 361 // Create the fragments and add as children of the view pager. 362 // The pager adapter will only change the visibility; it'll never create/destroy 363 // fragments. 364 // However, if it's after screen rotation, the fragments have been re-created by 365 // the fragment manager, so first see if there're already the target fragments 366 // existing. 367 mFavoritesFragment = (ContactTileListFragment) 368 fragmentManager.findFragmentByTag(FAVORITE_TAG); 369 mAllFragment = (DefaultContactBrowseListFragment) 370 fragmentManager.findFragmentByTag(ALL_TAG); 371 mGroupsFragment = (GroupBrowseListFragment) 372 fragmentManager.findFragmentByTag(GROUPS_TAG); 373 374 if (mFavoritesFragment == null) { 375 mFavoritesFragment = new ContactTileListFragment(); 376 mAllFragment = new DefaultContactBrowseListFragment(); 377 mGroupsFragment = new GroupBrowseListFragment(); 378 379 transaction.add(R.id.tab_pager, mFavoritesFragment, FAVORITE_TAG); 380 transaction.add(R.id.tab_pager, mAllFragment, ALL_TAG); 381 transaction.add(R.id.tab_pager, mGroupsFragment, GROUPS_TAG); 382 } 383 } 384 385 mFavoritesFragment.setListener(mFavoritesFragmentListener); 386 387 mAllFragment.setOnContactListActionListener(new ContactBrowserActionListener()); 388 389 mGroupsFragment.setListener(new GroupBrowserActionListener()); 390 391 // Hide all fragments for now. We adjust visibility when we get onSelectedTabChanged() 392 // from ActionBarAdapter. 393 transaction.hide(mFavoritesFragment); 394 transaction.hide(mAllFragment); 395 transaction.hide(mGroupsFragment); 396 397 if (isUsingTwoPanes) { 398 // Prepare 2-pane only fragments/views... 399 400 // Container views for fragments 401 mPeopleActivityView = getView(R.id.people_view); 402 mFavoritesView = getView(R.id.favorites_view); 403 mContactDetailsView = getView(R.id.contact_details_view); 404 mGroupDetailsView = getView(R.id.group_details_view); 405 mBrowserView = getView(R.id.browse_view); 406 407 // Only favorites tab with two panes has a separate frequent fragment 408 if (PhoneCapabilityTester.isUsingTwoPanesInFavorites(this)) { 409 mFrequentFragment = getFragment(R.id.frequent_fragment); 410 mFrequentFragment.setListener(mFavoritesFragmentListener); 411 mFrequentFragment.setDisplayType(DisplayType.FREQUENT_ONLY); 412 mFrequentFragment.enableQuickContact(true); 413 } 414 415 mContactDetailLoaderFragment = getFragment(R.id.contact_detail_loader_fragment); 416 mContactDetailLoaderFragment.setListener(mContactDetailLoaderFragmentListener); 417 418 mGroupDetailFragment = getFragment(R.id.group_detail_fragment); 419 mGroupDetailFragment.setListener(mGroupDetailFragmentListener); 420 mGroupDetailFragment.setQuickContact(true); 421 422 if (mContactDetailFragment != null) { 423 transaction.hide(mContactDetailFragment); 424 } 425 transaction.hide(mGroupDetailFragment); 426 427 // Configure contact details 428 mContactDetailLayoutController = new ContactDetailLayoutController(this, savedState, 429 getFragmentManager(), mContactDetailsView, 430 findViewById(R.id.contact_detail_container), 431 new ContactDetailFragmentListener()); 432 } 433 transaction.commitAllowingStateLoss(); 434 fragmentManager.executePendingTransactions(); 435 436 // Setting Properties after fragment is created 437 if (PhoneCapabilityTester.isUsingTwoPanesInFavorites(this)) { 438 mFavoritesFragment.enableQuickContact(true); 439 mFavoritesFragment.setDisplayType(DisplayType.STARRED_ONLY); 440 } else { 441 // For 2-pane in All and Groups but not in Favorites fragment, show the chevron 442 // for quick contact popup 443 mFavoritesFragment.enableQuickContact(isUsingTwoPanes); 444 mFavoritesFragment.setDisplayType(DisplayType.STREQUENT); 445 } 446 447 // Configure action bar 448 mActionBarAdapter = new ActionBarAdapter(this, this, getActionBar(), isUsingTwoPanes); 449 mActionBarAdapter.initialize(savedState, mRequest); 450 451 invalidateOptionsMenuIfNeeded(); 452 } 453 454 @Override 455 protected void onStart() { 456 if (!mFragmentInitialized) { 457 mFragmentInitialized = true; 458 /* Configure fragments if we haven't. 459 * 460 * Note it's a one-shot initialization, so we want to do this in {@link #onCreate}. 461 * 462 * However, because this method may indirectly touch views in fragments but fragments 463 * created in {@link #configureContentView} using a {@link FragmentTransaction} will NOT 464 * have views until {@link Activity#onCreate} finishes (they would if they were inflated 465 * from a layout), we need to do it here in {@link #onStart()}. 466 * 467 * (When {@link Fragment#onCreateView} is called is different in the former case and 468 * in the latter case, unfortunately.) 469 * 470 * Also, we skip most of the work in it if the activity is a re-created one. 471 * (so the argument.) 472 */ 473 configureFragments(!mIsRecreatedInstance); 474 } else if (PhoneCapabilityTester.isUsingTwoPanes(this) && !mCurrentFilterIsValid) { 475 // We only want to do the filter check in onStart for wide screen devices where it 476 // is often possible to get into single contact mode. Only do this check if 477 // the filter hasn't already been set properly (i.e. onCreate or onActivityResult). 478 479 // Since there is only one {@link ContactListFilterController} across multiple 480 // activity instances, make sure the filter controller is in sync withthe current 481 // contact list fragment filter. 482 // TODO: Clean this up. Perhaps change {@link ContactListFilterController} to not be a 483 // singleton? 484 mContactListFilterController.setContactListFilter(mAllFragment.getFilter(), true); 485 mContactListFilterController.checkFilterValidity(true); 486 mCurrentFilterIsValid = true; 487 } 488 super.onStart(); 489 } 490 491 @Override 492 protected void onPause() { 493 mOptionsMenuContactsAvailable = false; 494 mProviderStatusWatcher.stop(); 495 super.onPause(); 496 } 497 498 @Override 499 protected void onResume() { 500 super.onResume(); 501 502 mProviderStatusWatcher.start(); 503 updateViewConfiguration(true); 504 505 // Re-register the listener, which may have been cleared when onSaveInstanceState was 506 // called. See also: onSaveInstanceState 507 mActionBarAdapter.setListener(this); 508 mDisableOptionItemSelected = false; 509 if (mTabPager != null) { 510 mTabPager.setOnPageChangeListener(mTabPagerListener); 511 } 512 // Current tab may have changed since the last onSaveInstanceState(). Make sure 513 // the actual contents match the tab. 514 updateFragmentsVisibility(); 515 } 516 517 @Override 518 protected void onStop() { 519 super.onStop(); 520 mCurrentFilterIsValid = false; 521 } 522 523 @Override 524 protected void onDestroy() { 525 mProviderStatusWatcher.removeListener(this); 526 527 // Some of variables will be null if this Activity redirects Intent. 528 // See also onCreate() or other methods called during the Activity's initialization. 529 if (mActionBarAdapter != null) { 530 mActionBarAdapter.setListener(null); 531 } 532 if (mContactListFilterController != null) { 533 mContactListFilterController.removeListener(this); 534 } 535 536 super.onDestroy(); 537 } 538 539 private void configureFragments(boolean fromRequest) { 540 if (fromRequest) { 541 ContactListFilter filter = null; 542 int actionCode = mRequest.getActionCode(); 543 boolean searchMode = mRequest.isSearchMode(); 544 final int tabToOpen; 545 switch (actionCode) { 546 case ContactsRequest.ACTION_ALL_CONTACTS: 547 filter = ContactListFilter.createFilterWithType( 548 ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS); 549 tabToOpen = TabState.ALL; 550 break; 551 case ContactsRequest.ACTION_CONTACTS_WITH_PHONES: 552 filter = ContactListFilter.createFilterWithType( 553 ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY); 554 tabToOpen = TabState.ALL; 555 break; 556 557 case ContactsRequest.ACTION_FREQUENT: 558 case ContactsRequest.ACTION_STREQUENT: 559 case ContactsRequest.ACTION_STARRED: 560 tabToOpen = TabState.FAVORITES; 561 break; 562 case ContactsRequest.ACTION_VIEW_CONTACT: 563 // We redirect this intent to the detail activity on 1-pane, so we don't get 564 // here. It's only for 2-pane. 565 Uri currentlyLoadedContactUri = mContactDetailFragment.getUri(); 566 if (currentlyLoadedContactUri != null 567 && !mRequest.getContactUri().equals(currentlyLoadedContactUri)) { 568 mContactDetailsView.setMaskVisibility(true); 569 } 570 tabToOpen = TabState.ALL; 571 break; 572 case ContactsRequest.ACTION_GROUP: 573 tabToOpen = TabState.GROUPS; 574 break; 575 default: 576 tabToOpen = -1; 577 break; 578 } 579 if (tabToOpen != -1) { 580 mActionBarAdapter.setCurrentTab(tabToOpen); 581 } 582 583 if (filter != null) { 584 mContactListFilterController.setContactListFilter(filter, false); 585 searchMode = false; 586 } 587 588 if (mRequest.getContactUri() != null) { 589 searchMode = false; 590 } 591 592 mActionBarAdapter.setSearchMode(searchMode); 593 configureContactListFragmentForRequest(); 594 } 595 596 configureContactListFragment(); 597 configureGroupListFragment(); 598 599 invalidateOptionsMenuIfNeeded(); 600 } 601 602 @Override 603 public void onContactListFilterChanged() { 604 if (mAllFragment == null || !mAllFragment.isAdded()) { 605 return; 606 } 607 608 mAllFragment.setFilter(mContactListFilterController.getFilter()); 609 610 invalidateOptionsMenuIfNeeded(); 611 } 612 613 private void setupContactDetailFragment(final Uri contactLookupUri) { 614 mContactDetailLoaderFragment.loadUri(contactLookupUri); 615 invalidateOptionsMenuIfNeeded(); 616 } 617 618 private void setupGroupDetailFragment(Uri groupUri) { 619 // If we are switching from one group to another, do a cross-fade 620 if (mGroupDetailFragment != null && mGroupDetailFragment.getGroupUri() != null && 621 !UriUtils.areEqual(mGroupDetailFragment.getGroupUri(), groupUri)) { 622 mGroupDetailsView.startMaskTransition(false, -1); 623 } 624 mGroupDetailFragment.loadGroup(groupUri); 625 invalidateOptionsMenuIfNeeded(); 626 } 627 628 /** 629 * Handler for action bar actions. 630 */ 631 @Override 632 public void onAction(int action) { 633 switch (action) { 634 case ActionBarAdapter.Listener.Action.START_SEARCH_MODE: 635 // Tell the fragments that we're in the search mode 636 configureFragments(false /* from request */); 637 updateFragmentsVisibility(); 638 invalidateOptionsMenu(); 639 break; 640 case ActionBarAdapter.Listener.Action.STOP_SEARCH_MODE: 641 setQueryTextToFragment(""); 642 updateFragmentsVisibility(); 643 invalidateOptionsMenu(); 644 break; 645 case ActionBarAdapter.Listener.Action.CHANGE_SEARCH_QUERY: 646 final String queryString = mActionBarAdapter.getQueryString(); 647 setQueryTextToFragment(queryString); 648 updateDebugOptionsVisibility( 649 ENABLE_DEBUG_OPTIONS_HIDDEN_CODE.equals(queryString)); 650 break; 651 default: 652 throw new IllegalStateException("Unkonwn ActionBarAdapter action: " + action); 653 } 654 } 655 656 @Override 657 public void onSelectedTabChanged() { 658 updateFragmentsVisibility(); 659 } 660 661 private void updateDebugOptionsVisibility(boolean visible) { 662 if (mEnableDebugMenuOptions != visible) { 663 mEnableDebugMenuOptions = visible; 664 invalidateOptionsMenu(); 665 } 666 } 667 668 /** 669 * Updates the fragment/view visibility according to the current mode, such as 670 * {@link ActionBarAdapter#isSearchMode()} and {@link ActionBarAdapter#getCurrentTab()}. 671 */ 672 private void updateFragmentsVisibility() { 673 int tab = mActionBarAdapter.getCurrentTab(); 674 675 // We use ViewPager on 1-pane. 676 if (!PhoneCapabilityTester.isUsingTwoPanes(this)) { 677 if (mActionBarAdapter.isSearchMode()) { 678 mTabPagerAdapter.setSearchMode(true); 679 } else { 680 // No smooth scrolling if quitting from the search mode. 681 final boolean wasSearchMode = mTabPagerAdapter.isSearchMode(); 682 mTabPagerAdapter.setSearchMode(false); 683 if (mTabPager.getCurrentItem() != tab) { 684 mTabPager.setCurrentItem(tab, !wasSearchMode); 685 } 686 } 687 invalidateOptionsMenu(); 688 showEmptyStateForTab(tab); 689 if (tab == TabState.GROUPS) { 690 mGroupsFragment.setAddAccountsVisibility(!areGroupWritableAccountsAvailable()); 691 } 692 return; 693 } 694 695 // for the tablet... 696 697 // If in search mode, we use the all list + contact details to show the result. 698 if (mActionBarAdapter.isSearchMode()) { 699 tab = TabState.ALL; 700 } 701 702 switch (tab) { 703 case TabState.FAVORITES: 704 mFavoritesView.setVisibility(View.VISIBLE); 705 mBrowserView.setVisibility(View.GONE); 706 mGroupDetailsView.setVisibility(View.GONE); 707 mContactDetailsView.setVisibility(View.GONE); 708 break; 709 case TabState.GROUPS: 710 mFavoritesView.setVisibility(View.GONE); 711 mBrowserView.setVisibility(View.VISIBLE); 712 mGroupDetailsView.setVisibility(View.VISIBLE); 713 mContactDetailsView.setVisibility(View.GONE); 714 mGroupsFragment.setAddAccountsVisibility(!areGroupWritableAccountsAvailable()); 715 break; 716 case TabState.ALL: 717 mFavoritesView.setVisibility(View.GONE); 718 mBrowserView.setVisibility(View.VISIBLE); 719 mContactDetailsView.setVisibility(View.VISIBLE); 720 mGroupDetailsView.setVisibility(View.GONE); 721 break; 722 } 723 mPeopleActivityView.startMaskTransition(false, TAB_FADE_IN_DURATION); 724 FragmentManager fragmentManager = getFragmentManager(); 725 FragmentTransaction ft = fragmentManager.beginTransaction(); 726 727 // Note mContactDetailLoaderFragment is an invisible fragment, but we still have to show/ 728 // hide it so its options menu will be shown/hidden. 729 switch (tab) { 730 case TabState.FAVORITES: 731 showFragment(ft, mFavoritesFragment); 732 showFragment(ft, mFrequentFragment); 733 hideFragment(ft, mAllFragment); 734 hideFragment(ft, mContactDetailLoaderFragment); 735 hideFragment(ft, mContactDetailFragment); 736 hideFragment(ft, mGroupsFragment); 737 hideFragment(ft, mGroupDetailFragment); 738 break; 739 case TabState.ALL: 740 hideFragment(ft, mFavoritesFragment); 741 hideFragment(ft, mFrequentFragment); 742 showFragment(ft, mAllFragment); 743 showFragment(ft, mContactDetailLoaderFragment); 744 showFragment(ft, mContactDetailFragment); 745 hideFragment(ft, mGroupsFragment); 746 hideFragment(ft, mGroupDetailFragment); 747 break; 748 case TabState.GROUPS: 749 hideFragment(ft, mFavoritesFragment); 750 hideFragment(ft, mFrequentFragment); 751 hideFragment(ft, mAllFragment); 752 hideFragment(ft, mContactDetailLoaderFragment); 753 hideFragment(ft, mContactDetailFragment); 754 showFragment(ft, mGroupsFragment); 755 showFragment(ft, mGroupDetailFragment); 756 break; 757 } 758 if (!ft.isEmpty()) { 759 ft.commitAllowingStateLoss(); 760 fragmentManager.executePendingTransactions(); 761 // When switching tabs, we need to invalidate options menu, but executing a 762 // fragment transaction does it implicitly. We don't have to call invalidateOptionsMenu 763 // manually. 764 } 765 showEmptyStateForTab(tab); 766 } 767 768 private void showEmptyStateForTab(int tab) { 769 if (mContactsUnavailableFragment != null) { 770 switch (tab) { 771 case TabState.FAVORITES: 772 mContactsUnavailableFragment.setMessageText( 773 R.string.listTotalAllContactsZeroStarred, -1); 774 break; 775 case TabState.GROUPS: 776 mContactsUnavailableFragment.setMessageText(R.string.noGroups, 777 areGroupWritableAccountsAvailable() ? -1 : R.string.noAccounts); 778 break; 779 case TabState.ALL: 780 mContactsUnavailableFragment.setMessageText(R.string.noContacts, -1); 781 break; 782 } 783 } 784 } 785 786 private class TabPagerListener implements ViewPager.OnPageChangeListener { 787 788 // This package-protected constructor is here because of a possible compiler bug. 789 // PeopleActivity$1.class should be generated due to the private outer/inner class access 790 // needed here. But for some reason, PeopleActivity$1.class is missing. 791 // Since $1 class is needed as a jvm work around to get access to the inner class, 792 // changing the constructor to package-protected or public will solve the problem. 793 // To verify whether $1 class is needed, javap PeopleActivity$TabPagerListener and look for 794 // references to PeopleActivity$1. 795 // 796 // When the constructor is private and PeopleActivity$1.class is missing, proguard will 797 // correctly catch this and throw warnings and error out the build on user/userdebug builds. 798 // 799 // All private inner classes below also need this fix. 800 TabPagerListener() {} 801 802 @Override 803 public void onPageScrollStateChanged(int state) { 804 } 805 806 @Override 807 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 808 } 809 810 @Override 811 public void onPageSelected(int position) { 812 // Make sure not in the search mode, in which case position != TabState.ordinal(). 813 if (!mTabPagerAdapter.isSearchMode()) { 814 mActionBarAdapter.setCurrentTab(position, false); 815 showEmptyStateForTab(position); 816 if (position == TabState.GROUPS) { 817 mGroupsFragment.setAddAccountsVisibility(!areGroupWritableAccountsAvailable()); 818 } 819 invalidateOptionsMenu(); 820 } 821 } 822 } 823 824 /** 825 * Adapter for the {@link ViewPager}. Unlike {@link FragmentPagerAdapter}, 826 * {@link #instantiateItem} returns existing fragments, and {@link #instantiateItem}/ 827 * {@link #destroyItem} show/hide fragments instead of attaching/detaching. 828 * 829 * In search mode, we always show the "all" fragment, and disable the swipe. We change the 830 * number of items to 1 to disable the swipe. 831 * 832 * TODO figure out a more straight way to disable swipe. 833 */ 834 private class TabPagerAdapter extends PagerAdapter { 835 private final FragmentManager mFragmentManager; 836 private FragmentTransaction mCurTransaction = null; 837 838 private boolean mTabPagerAdapterSearchMode; 839 840 private Fragment mCurrentPrimaryItem; 841 842 public TabPagerAdapter() { 843 mFragmentManager = getFragmentManager(); 844 } 845 846 public boolean isSearchMode() { 847 return mTabPagerAdapterSearchMode; 848 } 849 850 public void setSearchMode(boolean searchMode) { 851 if (searchMode == mTabPagerAdapterSearchMode) { 852 return; 853 } 854 mTabPagerAdapterSearchMode = searchMode; 855 notifyDataSetChanged(); 856 } 857 858 @Override 859 public int getCount() { 860 return mTabPagerAdapterSearchMode ? 1 : TabState.COUNT; 861 } 862 863 /** Gets called when the number of items changes. */ 864 @Override 865 public int getItemPosition(Object object) { 866 if (mTabPagerAdapterSearchMode) { 867 if (object == mAllFragment) { 868 return 0; // Only 1 page in search mode 869 } 870 } else { 871 if (object == mFavoritesFragment) { 872 return TabState.FAVORITES; 873 } 874 if (object == mAllFragment) { 875 return TabState.ALL; 876 } 877 if (object == mGroupsFragment) { 878 return TabState.GROUPS; 879 } 880 } 881 return POSITION_NONE; 882 } 883 884 @Override 885 public void startUpdate(ViewGroup container) { 886 } 887 888 private Fragment getFragment(int position) { 889 if (mTabPagerAdapterSearchMode) { 890 if (position != 0) { 891 // This has only been observed in monkey tests. 892 // Let's log this issue, but not crash 893 Log.w(TAG, "Request fragment at position=" + position + ", eventhough we " + 894 "are in search mode"); 895 } 896 return mAllFragment; 897 } else { 898 if (position == TabState.FAVORITES) { 899 return mFavoritesFragment; 900 } else if (position == TabState.ALL) { 901 return mAllFragment; 902 } else if (position == TabState.GROUPS) { 903 return mGroupsFragment; 904 } 905 } 906 throw new IllegalArgumentException("position: " + position); 907 } 908 909 @Override 910 public Object instantiateItem(ViewGroup container, int position) { 911 if (mCurTransaction == null) { 912 mCurTransaction = mFragmentManager.beginTransaction(); 913 } 914 Fragment f = getFragment(position); 915 mCurTransaction.show(f); 916 917 // Non primary pages are not visible. 918 f.setUserVisibleHint(f == mCurrentPrimaryItem); 919 return f; 920 } 921 922 @Override 923 public void destroyItem(ViewGroup container, int position, Object object) { 924 if (mCurTransaction == null) { 925 mCurTransaction = mFragmentManager.beginTransaction(); 926 } 927 mCurTransaction.hide((Fragment) object); 928 } 929 930 @Override 931 public void finishUpdate(ViewGroup container) { 932 if (mCurTransaction != null) { 933 mCurTransaction.commitAllowingStateLoss(); 934 mCurTransaction = null; 935 mFragmentManager.executePendingTransactions(); 936 } 937 } 938 939 @Override 940 public boolean isViewFromObject(View view, Object object) { 941 return ((Fragment) object).getView() == view; 942 } 943 944 @Override 945 public void setPrimaryItem(ViewGroup container, int position, Object object) { 946 Fragment fragment = (Fragment) object; 947 if (mCurrentPrimaryItem != fragment) { 948 if (mCurrentPrimaryItem != null) { 949 mCurrentPrimaryItem.setUserVisibleHint(false); 950 } 951 if (fragment != null) { 952 fragment.setUserVisibleHint(true); 953 } 954 mCurrentPrimaryItem = fragment; 955 } 956 } 957 958 @Override 959 public Parcelable saveState() { 960 return null; 961 } 962 963 @Override 964 public void restoreState(Parcelable state, ClassLoader loader) { 965 } 966 } 967 968 private void setQueryTextToFragment(String query) { 969 mAllFragment.setQueryString(query, true); 970 mAllFragment.setVisibleScrollbarEnabled(!mAllFragment.isSearchMode()); 971 } 972 973 private void configureContactListFragmentForRequest() { 974 Uri contactUri = mRequest.getContactUri(); 975 if (contactUri != null) { 976 // For an incoming request, explicitly require a selection if we are on 2-pane UI, 977 // (i.e. even if we view the same selected contact, the contact may no longer be 978 // in the list, so we must refresh the list). 979 if (PhoneCapabilityTester.isUsingTwoPanes(this)) { 980 mAllFragment.setSelectionRequired(true); 981 } 982 mAllFragment.setSelectedContactUri(contactUri); 983 } 984 985 mAllFragment.setFilter(mContactListFilterController.getFilter()); 986 setQueryTextToFragment(mActionBarAdapter.getQueryString()); 987 988 if (mRequest.isDirectorySearchEnabled()) { 989 mAllFragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_DEFAULT); 990 } else { 991 mAllFragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE); 992 } 993 } 994 995 private void configureContactListFragment() { 996 // Filter may be changed when this Activity is in background. 997 mAllFragment.setFilter(mContactListFilterController.getFilter()); 998 999 final boolean useTwoPane = PhoneCapabilityTester.isUsingTwoPanes(this); 1000 1001 mAllFragment.setVerticalScrollbarPosition(getScrollBarPosition(useTwoPane)); 1002 mAllFragment.setSelectionVisible(useTwoPane); 1003 mAllFragment.setQuickContactEnabled(!useTwoPane); 1004 } 1005 1006 private int getScrollBarPosition(boolean useTwoPane) { 1007 final boolean isLayoutRtl = isRTL(); 1008 final int position; 1009 if (useTwoPane) { 1010 position = isLayoutRtl ? View.SCROLLBAR_POSITION_RIGHT : View.SCROLLBAR_POSITION_LEFT; 1011 } else { 1012 position = isLayoutRtl ? View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT; 1013 } 1014 return position; 1015 } 1016 1017 private boolean isRTL() { 1018 final Locale locale = Locale.getDefault(); 1019 return TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL; 1020 } 1021 1022 private void configureGroupListFragment() { 1023 final boolean useTwoPane = PhoneCapabilityTester.isUsingTwoPanes(this); 1024 mGroupsFragment.setVerticalScrollbarPosition(getScrollBarPosition(useTwoPane)); 1025 mGroupsFragment.setSelectionVisible(useTwoPane); 1026 } 1027 1028 @Override 1029 public void onProviderStatusChange() { 1030 updateViewConfiguration(false); 1031 } 1032 1033 private void updateViewConfiguration(boolean forceUpdate) { 1034 ProviderStatusWatcher.Status providerStatus = mProviderStatusWatcher.getProviderStatus(); 1035 if (!forceUpdate && (mProviderStatus != null) 1036 && (providerStatus.status == mProviderStatus.status)) return; 1037 mProviderStatus = providerStatus; 1038 1039 View contactsUnavailableView = findViewById(R.id.contacts_unavailable_view); 1040 View mainView = findViewById(R.id.main_view); 1041 1042 if (mProviderStatus.status == ProviderStatus.STATUS_NORMAL) { 1043 // Ensure that the mTabPager is visible; we may have made it invisible below. 1044 contactsUnavailableView.setVisibility(View.GONE); 1045 if (mTabPager != null) { 1046 mTabPager.setVisibility(View.VISIBLE); 1047 } 1048 1049 if (mainView != null) { 1050 mainView.setVisibility(View.VISIBLE); 1051 } 1052 if (mAllFragment != null) { 1053 mAllFragment.setEnabled(true); 1054 } 1055 } else { 1056 // If there are no accounts on the device and we should show the "no account" prompt 1057 // (based on {@link SharedPreferences}), then launch the account setup activity so the 1058 // user can sign-in or create an account. 1059 // 1060 // Also check for ability to modify accounts. In limited user mode, you can't modify 1061 // accounts so there is no point sending users to account setup activity. 1062 final UserManager userManager = UserManager.get(this); 1063 final boolean disallowModifyAccounts = userManager.getUserRestrictions().getBoolean( 1064 UserManager.DISALLOW_MODIFY_ACCOUNTS); 1065 if (!disallowModifyAccounts && !areContactWritableAccountsAvailable() && 1066 AccountPromptUtils.shouldShowAccountPrompt(this)) { 1067 AccountPromptUtils.launchAccountPrompt(this); 1068 return; 1069 } 1070 1071 // Otherwise, continue setting up the page so that the user can still use the app 1072 // without an account. 1073 if (mAllFragment != null) { 1074 mAllFragment.setEnabled(false); 1075 } 1076 if (mContactsUnavailableFragment == null) { 1077 mContactsUnavailableFragment = new ContactsUnavailableFragment(); 1078 mContactsUnavailableFragment.setOnContactsUnavailableActionListener( 1079 new ContactsUnavailableFragmentListener()); 1080 getFragmentManager().beginTransaction() 1081 .replace(R.id.contacts_unavailable_container, mContactsUnavailableFragment) 1082 .commitAllowingStateLoss(); 1083 } 1084 mContactsUnavailableFragment.updateStatus(mProviderStatus); 1085 1086 // Show the contactsUnavailableView, and hide the mTabPager so that we don't 1087 // see it sliding in underneath the contactsUnavailableView at the edges. 1088 contactsUnavailableView.setVisibility(View.VISIBLE); 1089 if (mTabPager != null) { 1090 mTabPager.setVisibility(View.GONE); 1091 } 1092 1093 if (mainView != null) { 1094 mainView.setVisibility(View.INVISIBLE); 1095 } 1096 1097 showEmptyStateForTab(mActionBarAdapter.getCurrentTab()); 1098 } 1099 1100 invalidateOptionsMenuIfNeeded(); 1101 } 1102 1103 private final class ContactBrowserActionListener implements OnContactBrowserActionListener { 1104 ContactBrowserActionListener() {} 1105 1106 @Override 1107 public void onSelectionChange() { 1108 if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) { 1109 setupContactDetailFragment(mAllFragment.getSelectedContactUri()); 1110 } 1111 } 1112 1113 @Override 1114 public void onViewContactAction(Uri contactLookupUri) { 1115 if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) { 1116 setupContactDetailFragment(contactLookupUri); 1117 } else { 1118 Intent intent = new Intent(Intent.ACTION_VIEW, contactLookupUri); 1119 startActivity(intent); 1120 } 1121 } 1122 1123 @Override 1124 public void onCreateNewContactAction() { 1125 Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 1126 Bundle extras = getIntent().getExtras(); 1127 if (extras != null) { 1128 intent.putExtras(extras); 1129 } 1130 startActivity(intent); 1131 } 1132 1133 @Override 1134 public void onEditContactAction(Uri contactLookupUri) { 1135 Intent intent = new Intent(Intent.ACTION_EDIT, contactLookupUri); 1136 Bundle extras = getIntent().getExtras(); 1137 if (extras != null) { 1138 intent.putExtras(extras); 1139 } 1140 intent.putExtra( 1141 ContactEditorActivity.INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, true); 1142 startActivityForResult(intent, SUBACTIVITY_EDIT_CONTACT); 1143 } 1144 1145 @Override 1146 public void onAddToFavoritesAction(Uri contactUri) { 1147 ContentValues values = new ContentValues(1); 1148 values.put(Contacts.STARRED, 1); 1149 getContentResolver().update(contactUri, values, null, null); 1150 } 1151 1152 @Override 1153 public void onRemoveFromFavoritesAction(Uri contactUri) { 1154 ContentValues values = new ContentValues(1); 1155 values.put(Contacts.STARRED, 0); 1156 getContentResolver().update(contactUri, values, null, null); 1157 } 1158 1159 @Override 1160 public void onDeleteContactAction(Uri contactUri) { 1161 ContactDeletionInteraction.start(PeopleActivity.this, contactUri, false); 1162 } 1163 1164 @Override 1165 public void onFinishAction() { 1166 onBackPressed(); 1167 } 1168 1169 @Override 1170 public void onInvalidSelection() { 1171 ContactListFilter filter; 1172 ContactListFilter currentFilter = mAllFragment.getFilter(); 1173 if (currentFilter != null 1174 && currentFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { 1175 filter = ContactListFilter.createFilterWithType( 1176 ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS); 1177 mAllFragment.setFilter(filter); 1178 } else { 1179 filter = ContactListFilter.createFilterWithType( 1180 ContactListFilter.FILTER_TYPE_SINGLE_CONTACT); 1181 mAllFragment.setFilter(filter, false); 1182 } 1183 mContactListFilterController.setContactListFilter(filter, true); 1184 } 1185 } 1186 1187 private class ContactDetailLoaderFragmentListener implements ContactLoaderFragmentListener { 1188 ContactDetailLoaderFragmentListener() {} 1189 1190 @Override 1191 public void onContactNotFound() { 1192 // Nothing needs to be done here 1193 } 1194 1195 @Override 1196 public void onDetailsLoaded(final Contact result) { 1197 if (result == null) { 1198 // Nothing is loaded. Show empty state. 1199 mContactDetailLayoutController.showEmptyState(); 1200 return; 1201 } 1202 // Since {@link FragmentTransaction}s cannot be done in the onLoadFinished() of the 1203 // {@link LoaderCallbacks}, then post this {@link Runnable} to the {@link Handler} 1204 // on the main thread to execute later. 1205 mHandler.post(new Runnable() { 1206 @Override 1207 public void run() { 1208 // If the activity is destroyed (or will be destroyed soon), don't update the UI 1209 if (isFinishing()) { 1210 return; 1211 } 1212 mContactDetailLayoutController.setContactData(result); 1213 } 1214 }); 1215 } 1216 1217 @Override 1218 public void onEditRequested(Uri contactLookupUri) { 1219 Intent intent = new Intent(Intent.ACTION_EDIT, contactLookupUri); 1220 intent.putExtra( 1221 ContactEditorActivity.INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, true); 1222 startActivityForResult(intent, SUBACTIVITY_EDIT_CONTACT); 1223 } 1224 1225 @Override 1226 public void onDeleteRequested(Uri contactUri) { 1227 ContactDeletionInteraction.start(PeopleActivity.this, contactUri, false); 1228 } 1229 } 1230 1231 public class ContactDetailFragmentListener implements ContactDetailFragment.Listener { 1232 @Override 1233 public void onItemClicked(Intent intent) { 1234 if (intent == null) { 1235 return; 1236 } 1237 try { 1238 startActivity(intent); 1239 } catch (ActivityNotFoundException e) { 1240 Log.e(TAG, "No activity found for intent: " + intent); 1241 } 1242 } 1243 1244 @Override 1245 public void onCreateRawContactRequested(ArrayList<ContentValues> values, 1246 AccountWithDataSet account) { 1247 Toast.makeText(PeopleActivity.this, R.string.toast_making_personal_copy, 1248 Toast.LENGTH_LONG).show(); 1249 Intent serviceIntent = ContactSaveService.createNewRawContactIntent( 1250 PeopleActivity.this, values, account, 1251 PeopleActivity.class, Intent.ACTION_VIEW); 1252 startService(serviceIntent); 1253 } 1254 } 1255 1256 private class ContactsUnavailableFragmentListener 1257 implements OnContactsUnavailableActionListener { 1258 ContactsUnavailableFragmentListener() {} 1259 1260 @Override 1261 public void onCreateNewContactAction() { 1262 startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI)); 1263 } 1264 1265 @Override 1266 public void onAddAccountAction() { 1267 Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT); 1268 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 1269 intent.putExtra(Settings.EXTRA_AUTHORITIES, 1270 new String[] { ContactsContract.AUTHORITY }); 1271 startActivity(intent); 1272 } 1273 1274 @Override 1275 public void onImportContactsFromFileAction() { 1276 ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable(), 1277 PeopleActivity.class); 1278 } 1279 1280 @Override 1281 public void onFreeInternalStorageAction() { 1282 startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS)); 1283 } 1284 } 1285 1286 private final class StrequentContactListFragmentListener 1287 implements ContactTileListFragment.Listener { 1288 StrequentContactListFragmentListener() {} 1289 1290 @Override 1291 public void onContactSelected(Uri contactUri, Rect targetRect) { 1292 if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) { 1293 QuickContact.showQuickContact(PeopleActivity.this, targetRect, contactUri, 0, null); 1294 } else { 1295 startActivity(new Intent(Intent.ACTION_VIEW, contactUri)); 1296 } 1297 } 1298 1299 @Override 1300 public void onCallNumberDirectly(String phoneNumber) { 1301 // No need to call phone number directly from People app. 1302 Log.w(TAG, "unexpected invocation of onCallNumberDirectly()"); 1303 } 1304 } 1305 1306 private final class GroupBrowserActionListener implements OnGroupBrowserActionListener { 1307 1308 GroupBrowserActionListener() {} 1309 1310 @Override 1311 public void onViewGroupAction(Uri groupUri) { 1312 if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) { 1313 setupGroupDetailFragment(groupUri); 1314 } else { 1315 Intent intent = new Intent(PeopleActivity.this, GroupDetailActivity.class); 1316 intent.setData(groupUri); 1317 startActivity(intent); 1318 } 1319 } 1320 } 1321 1322 private class GroupDetailFragmentListener implements GroupDetailFragment.Listener { 1323 1324 GroupDetailFragmentListener() {} 1325 1326 @Override 1327 public void onGroupSizeUpdated(String size) { 1328 // Nothing needs to be done here because the size will be displayed in the detail 1329 // fragment 1330 } 1331 1332 @Override 1333 public void onGroupTitleUpdated(String title) { 1334 // Nothing needs to be done here because the title will be displayed in the detail 1335 // fragment 1336 } 1337 1338 @Override 1339 public void onAccountTypeUpdated(String accountTypeString, String dataSet) { 1340 // Nothing needs to be done here because the group source will be displayed in the 1341 // detail fragment 1342 } 1343 1344 @Override 1345 public void onEditRequested(Uri groupUri) { 1346 final Intent intent = new Intent(PeopleActivity.this, GroupEditorActivity.class); 1347 intent.setData(groupUri); 1348 intent.setAction(Intent.ACTION_EDIT); 1349 startActivityForResult(intent, SUBACTIVITY_EDIT_GROUP); 1350 } 1351 1352 @Override 1353 public void onContactSelected(Uri contactUri) { 1354 // Nothing needs to be done here because either quickcontact will be displayed 1355 // or activity will take care of selection 1356 } 1357 } 1358 1359 public void startActivityAndForwardResult(final Intent intent) { 1360 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 1361 1362 // Forward extras to the new activity 1363 Bundle extras = getIntent().getExtras(); 1364 if (extras != null) { 1365 intent.putExtras(extras); 1366 } 1367 startActivity(intent); 1368 finish(); 1369 } 1370 1371 @Override 1372 public boolean onCreateOptionsMenu(Menu menu) { 1373 if (!areContactsAvailable()) { 1374 // If contacts aren't available, hide all menu items. 1375 return false; 1376 } 1377 super.onCreateOptionsMenu(menu); 1378 1379 MenuInflater inflater = getMenuInflater(); 1380 inflater.inflate(R.menu.people_options, menu); 1381 1382 return true; 1383 } 1384 1385 private void invalidateOptionsMenuIfNeeded() { 1386 if (isOptionsMenuChanged()) { 1387 invalidateOptionsMenu(); 1388 } 1389 } 1390 1391 public boolean isOptionsMenuChanged() { 1392 if (mOptionsMenuContactsAvailable != areContactsAvailable()) { 1393 return true; 1394 } 1395 1396 if (mAllFragment != null && mAllFragment.isOptionsMenuChanged()) { 1397 return true; 1398 } 1399 1400 if (mContactDetailLoaderFragment != null && 1401 mContactDetailLoaderFragment.isOptionsMenuChanged()) { 1402 return true; 1403 } 1404 1405 if (mGroupDetailFragment != null && mGroupDetailFragment.isOptionsMenuChanged()) { 1406 return true; 1407 } 1408 1409 return false; 1410 } 1411 1412 @Override 1413 public boolean onPrepareOptionsMenu(Menu menu) { 1414 mOptionsMenuContactsAvailable = areContactsAvailable(); 1415 if (!mOptionsMenuContactsAvailable) { 1416 return false; 1417 } 1418 1419 // Get references to individual menu items in the menu 1420 final MenuItem addContactMenu = menu.findItem(R.id.menu_add_contact); 1421 final MenuItem contactsFilterMenu = menu.findItem(R.id.menu_contacts_filter); 1422 1423 MenuItem addGroupMenu = menu.findItem(R.id.menu_add_group); 1424 1425 final MenuItem clearFrequentsMenu = menu.findItem(R.id.menu_clear_frequents); 1426 final MenuItem helpMenu = menu.findItem(R.id.menu_help); 1427 1428 final boolean isSearchMode = mActionBarAdapter.isSearchMode(); 1429 if (isSearchMode) { 1430 addContactMenu.setVisible(false); 1431 addGroupMenu.setVisible(false); 1432 contactsFilterMenu.setVisible(false); 1433 clearFrequentsMenu.setVisible(false); 1434 helpMenu.setVisible(false); 1435 } else { 1436 switch (mActionBarAdapter.getCurrentTab()) { 1437 case TabState.FAVORITES: 1438 addContactMenu.setVisible(true); 1439 addGroupMenu.setVisible(false); 1440 contactsFilterMenu.setVisible(false); 1441 clearFrequentsMenu.setVisible(hasFrequents()); 1442 break; 1443 case TabState.ALL: 1444 addContactMenu.setVisible(true); 1445 addGroupMenu.setVisible(false); 1446 contactsFilterMenu.setVisible(true); 1447 clearFrequentsMenu.setVisible(false); 1448 break; 1449 case TabState.GROUPS: 1450 // Do not display the "new group" button if no accounts are available 1451 if (areGroupWritableAccountsAvailable()) { 1452 addGroupMenu.setVisible(true); 1453 } else { 1454 addGroupMenu.setVisible(false); 1455 } 1456 addContactMenu.setVisible(false); 1457 contactsFilterMenu.setVisible(false); 1458 clearFrequentsMenu.setVisible(false); 1459 break; 1460 } 1461 HelpUtils.prepareHelpMenuItem(this, helpMenu, R.string.help_url_people_main); 1462 } 1463 final boolean showMiscOptions = !isSearchMode; 1464 makeMenuItemVisible(menu, R.id.menu_search, showMiscOptions); 1465 makeMenuItemVisible(menu, R.id.menu_import_export, showMiscOptions); 1466 makeMenuItemVisible(menu, R.id.menu_accounts, showMiscOptions); 1467 makeMenuItemVisible(menu, R.id.menu_settings, 1468 showMiscOptions && !ContactsPreferenceActivity.isEmpty(this)); 1469 1470 // Debug options need to be visible even in search mode. 1471 makeMenuItemVisible(menu, R.id.export_database, mEnableDebugMenuOptions); 1472 1473 return true; 1474 } 1475 1476 /** 1477 * Returns whether there are any frequently contacted people being displayed 1478 * @return 1479 */ 1480 private boolean hasFrequents() { 1481 if (PhoneCapabilityTester.isUsingTwoPanesInFavorites(this)) { 1482 return mFrequentFragment.hasFrequents(); 1483 } else { 1484 return mFavoritesFragment.hasFrequents(); 1485 } 1486 } 1487 1488 private void makeMenuItemVisible(Menu menu, int itemId, boolean visible) { 1489 MenuItem item =menu.findItem(itemId); 1490 if (item != null) { 1491 item.setVisible(visible); 1492 } 1493 } 1494 1495 @Override 1496 public boolean onOptionsItemSelected(MenuItem item) { 1497 if (mDisableOptionItemSelected) { 1498 return false; 1499 } 1500 1501 switch (item.getItemId()) { 1502 case android.R.id.home: { 1503 // The home icon on the action bar is pressed 1504 if (mActionBarAdapter.isUpShowing()) { 1505 // "UP" icon press -- should be treated as "back". 1506 onBackPressed(); 1507 } 1508 return true; 1509 } 1510 case R.id.menu_settings: { 1511 final Intent intent = new Intent(this, ContactsPreferenceActivity.class); 1512 // as there is only one section right now, make sure it is selected 1513 // on small screens, this also hides the section selector 1514 // Due to b/5045558, this code unfortunately only works properly on phones 1515 boolean settingsAreMultiPane = getResources().getBoolean( 1516 com.android.internal.R.bool.preferences_prefer_dual_pane); 1517 if (!settingsAreMultiPane) { 1518 intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, 1519 DisplayOptionsPreferenceFragment.class.getName()); 1520 intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE, 1521 R.string.activity_title_settings); 1522 } 1523 startActivity(intent); 1524 return true; 1525 } 1526 case R.id.menu_contacts_filter: { 1527 AccountFilterUtil.startAccountFilterActivityForResult( 1528 this, SUBACTIVITY_ACCOUNT_FILTER, 1529 mContactListFilterController.getFilter()); 1530 return true; 1531 } 1532 case R.id.menu_search: { 1533 onSearchRequested(); 1534 return true; 1535 } 1536 case R.id.menu_add_contact: { 1537 final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 1538 // On 2-pane UI, we can let the editor activity finish itself and return 1539 // to this activity to display the new contact. 1540 if (PhoneCapabilityTester.isUsingTwoPanes(this)) { 1541 intent.putExtra( 1542 ContactEditorActivity.INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, 1543 true); 1544 startActivityForResult(intent, SUBACTIVITY_NEW_CONTACT); 1545 } else { 1546 // Otherwise, on 1-pane UI, we need the editor to launch the view contact 1547 // intent itself. 1548 startActivity(intent); 1549 } 1550 return true; 1551 } 1552 case R.id.menu_add_group: { 1553 createNewGroup(); 1554 return true; 1555 } 1556 case R.id.menu_import_export: { 1557 ImportExportDialogFragment.show(getFragmentManager(), areContactsAvailable(), 1558 PeopleActivity.class); 1559 return true; 1560 } 1561 case R.id.menu_clear_frequents: { 1562 ClearFrequentsDialog.show(getFragmentManager()); 1563 return true; 1564 } 1565 case R.id.menu_accounts: { 1566 final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS); 1567 intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] { 1568 ContactsContract.AUTHORITY 1569 }); 1570 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 1571 startActivity(intent); 1572 return true; 1573 } 1574 case R.id.export_database: { 1575 final Intent intent = new Intent("com.android.providers.contacts.DUMP_DATABASE"); 1576 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 1577 startActivity(intent); 1578 return true; 1579 } 1580 } 1581 return false; 1582 } 1583 1584 private void createNewGroup() { 1585 final Intent intent = new Intent(this, GroupEditorActivity.class); 1586 intent.setAction(Intent.ACTION_INSERT); 1587 startActivityForResult(intent, SUBACTIVITY_NEW_GROUP); 1588 } 1589 1590 @Override 1591 public boolean onSearchRequested() { // Search key pressed. 1592 mActionBarAdapter.setSearchMode(true); 1593 return true; 1594 } 1595 1596 @Override 1597 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 1598 switch (requestCode) { 1599 case SUBACTIVITY_ACCOUNT_FILTER: { 1600 AccountFilterUtil.handleAccountFilterResult( 1601 mContactListFilterController, resultCode, data); 1602 break; 1603 } 1604 1605 case SUBACTIVITY_NEW_CONTACT: 1606 case SUBACTIVITY_EDIT_CONTACT: { 1607 if (resultCode == RESULT_OK && PhoneCapabilityTester.isUsingTwoPanes(this)) { 1608 mRequest.setActionCode(ContactsRequest.ACTION_VIEW_CONTACT); 1609 mAllFragment.setSelectionRequired(true); 1610 mAllFragment.setSelectedContactUri(data.getData()); 1611 // Suppress IME if in search mode 1612 if (mActionBarAdapter != null) { 1613 mActionBarAdapter.clearFocusOnSearchView(); 1614 } 1615 // No need to change the contact filter 1616 mCurrentFilterIsValid = true; 1617 } 1618 break; 1619 } 1620 1621 case SUBACTIVITY_NEW_GROUP: 1622 case SUBACTIVITY_EDIT_GROUP: { 1623 if (resultCode == RESULT_OK && PhoneCapabilityTester.isUsingTwoPanes(this)) { 1624 mRequest.setActionCode(ContactsRequest.ACTION_GROUP); 1625 mGroupsFragment.setSelectedUri(data.getData()); 1626 } 1627 break; 1628 } 1629 1630 // TODO: Using the new startActivityWithResultFromFragment API this should not be needed 1631 // anymore 1632 case ContactEntryListFragment.ACTIVITY_REQUEST_CODE_PICKER: 1633 if (resultCode == RESULT_OK) { 1634 mAllFragment.onPickerResult(data); 1635 } 1636 1637 // TODO fix or remove multipicker code 1638 // else if (resultCode == RESULT_CANCELED && mMode == MODE_PICK_MULTIPLE_PHONES) { 1639 // // Finish the activity if the sub activity was canceled as back key is used 1640 // // to confirm user selection in MODE_PICK_MULTIPLE_PHONES. 1641 // finish(); 1642 // } 1643 // break; 1644 } 1645 } 1646 1647 @Override 1648 public boolean onKeyDown(int keyCode, KeyEvent event) { 1649 // TODO move to the fragment 1650 switch (keyCode) { 1651 // case KeyEvent.KEYCODE_CALL: { 1652 // if (callSelection()) { 1653 // return true; 1654 // } 1655 // break; 1656 // } 1657 1658 case KeyEvent.KEYCODE_DEL: { 1659 if (deleteSelection()) { 1660 return true; 1661 } 1662 break; 1663 } 1664 default: { 1665 // Bring up the search UI if the user starts typing 1666 final int unicodeChar = event.getUnicodeChar(); 1667 if ((unicodeChar != 0) 1668 // If COMBINING_ACCENT is set, it's not a unicode character. 1669 && ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) == 0) 1670 && !Character.isWhitespace(unicodeChar)) { 1671 String query = new String(new int[]{ unicodeChar }, 0, 1); 1672 if (!mActionBarAdapter.isSearchMode()) { 1673 mActionBarAdapter.setQueryString(query); 1674 mActionBarAdapter.setSearchMode(true); 1675 return true; 1676 } 1677 } 1678 } 1679 } 1680 1681 return super.onKeyDown(keyCode, event); 1682 } 1683 1684 @Override 1685 public void onBackPressed() { 1686 if (mActionBarAdapter.isSearchMode()) { 1687 mActionBarAdapter.setSearchMode(false); 1688 } else { 1689 super.onBackPressed(); 1690 } 1691 } 1692 1693 private boolean deleteSelection() { 1694 // TODO move to the fragment 1695 // if (mActionCode == ContactsRequest.ACTION_DEFAULT) { 1696 // final int position = mListView.getSelectedItemPosition(); 1697 // if (position != ListView.INVALID_POSITION) { 1698 // Uri contactUri = getContactUri(position); 1699 // if (contactUri != null) { 1700 // doContactDelete(contactUri); 1701 // return true; 1702 // } 1703 // } 1704 // } 1705 return false; 1706 } 1707 1708 @Override 1709 protected void onSaveInstanceState(Bundle outState) { 1710 super.onSaveInstanceState(outState); 1711 mActionBarAdapter.onSaveInstanceState(outState); 1712 if (mContactDetailLayoutController != null) { 1713 mContactDetailLayoutController.onSaveInstanceState(outState); 1714 } 1715 1716 // Clear the listener to make sure we don't get callbacks after onSaveInstanceState, 1717 // in order to avoid doing fragment transactions after it. 1718 // TODO Figure out a better way to deal with the issue. 1719 mDisableOptionItemSelected = true; 1720 mActionBarAdapter.setListener(null); 1721 if (mTabPager != null) { 1722 mTabPager.setOnPageChangeListener(null); 1723 } 1724 } 1725 1726 @Override 1727 protected void onRestoreInstanceState(Bundle savedInstanceState) { 1728 super.onRestoreInstanceState(savedInstanceState); 1729 // In our own lifecycle, the focus is saved and restore but later taken away by the 1730 // ViewPager. As a hack, we force focus on the SearchView if we know that we are searching. 1731 // This fixes the keyboard going away on screen rotation 1732 if (mActionBarAdapter.isSearchMode()) { 1733 mActionBarAdapter.setFocusOnSearchView(); 1734 } 1735 } 1736 1737 @Override 1738 public DialogManager getDialogManager() { 1739 return mDialogManager; 1740 } 1741 1742 // Visible for testing 1743 public ContactBrowseListFragment getListFragment() { 1744 return mAllFragment; 1745 } 1746 1747 // Visible for testing 1748 public ContactDetailFragment getDetailFragment() { 1749 return mContactDetailFragment; 1750 } 1751 } 1752