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